diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000000..b0e422abef --- /dev/null +++ b/.coveragerc @@ -0,0 +1,7 @@ +[run] +omit = + ./nuclei/* + ./routers/* + ./setup.py + ./tests/* + ./env/* diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000..eabfb03301 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,21 @@ +**/data/ +**/*.log +**/*.png +**/*.pstats +**/*.ipynb +**/bittensor.egg-info/* +**/lib/* +**/build/* +**/dist/* +**/runs/* +**/env/* +**/venv/* +**/tmp/* +**/test_results/* +**/__pycache__/* +**/.circleci +**/.git +**/.github +**/.hypothesis +**/.vscode +**/.gitignore diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000000..6b2eaa0333 --- /dev/null +++ b/.flake8 @@ -0,0 +1,4 @@ +[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/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml new file mode 100644 index 0000000000..5e875de9a0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -0,0 +1,59 @@ +name: Bug report +description: Create a report to help us improve +labels: [bug] +assignees: [] + +body: + - type: textarea + id: bug-description + attributes: + label: Describe the bug + description: A clear and concise description of what the bug is. + validations: + required: true + + - type: textarea + id: reproduce + attributes: + label: To Reproduce + description: Steps to reproduce the behavior. + placeholder: | + 1. Go to '...' + 2. Run command '...' + 3. Scroll down to '....' + 4. See error + validations: + required: true + + - type: textarea + id: expected-behavior + attributes: + label: Expected behavior + description: A clear and concise description of what you expected to happen. + validations: + required: true + + - type: textarea + id: screenshots + attributes: + label: Screenshots + description: If applicable, add screenshots to help explain your problem. + validations: + required: false + + - type: input + id: environment + attributes: + label: Environment + description: Please specify your OS and Distro, and Bittensor Version. + placeholder: "OS and Distro: [e.g. Linux Ubuntu, Linux Fedora, etc.], Bittensor Version [e.g. 22]" + validations: + required: true + + - type: textarea + id: additional-context + attributes: + label: Additional context + description: Add any other context about the problem here. + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml new file mode 100644 index 0000000000..b9cd275add --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yaml @@ -0,0 +1,38 @@ +name: Feature request +description: Suggest an idea for this project +labels: [feature] +assignees: [] + +body: + - type: textarea + id: problem-description + attributes: + label: Is your feature request related to a problem? Please describe. + description: A clear and concise description of what the problem is. + placeholder: "Ex. I'm always frustrated when [...]" + validations: + required: true + + - type: textarea + id: solution + attributes: + label: Describe the solution you'd like + description: A clear and concise description of what you want to happen. + validations: + required: true + + - type: textarea + id: alternatives + attributes: + label: Describe alternatives you've considered + description: A clear and concise description of any alternative solutions or features you've considered. + validations: + required: false + + - type: textarea + id: additional-context + attributes: + label: Additional context + description: Add any other context or screenshots about the feature request here. + validations: + required: false diff --git a/.github/PULL_REQUEST_TEMPLATE/bug_fix.md b/.github/PULL_REQUEST_TEMPLATE/bug_fix.md new file mode 100644 index 0000000000..e3c29ef96d --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/bug_fix.md @@ -0,0 +1,63 @@ + + +### Bug + + + +### Description of the Change + + + +### Alternate Designs + + + +### Possible Drawbacks + + + +### Verification Process + + + +### Release Notes + + + + +### Branch Acknowledgement +[ ] I am acknowledging that I am opening this branch against `staging` diff --git a/.github/PULL_REQUEST_TEMPLATE/feature_change.md b/.github/PULL_REQUEST_TEMPLATE/feature_change.md new file mode 100644 index 0000000000..15292a9d7a --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/feature_change.md @@ -0,0 +1,58 @@ +### Requirements for Adding, Changing, or Removing a Feature + +* Fill out the template below. Any pull request that does not include enough information to be reviewed in a timely manner may be closed at the maintainers' discretion. +* The pull request must contribute a change that has been endorsed by the maintainer team. See details in the template below. +* The pull request must update the test suite to exercise the updated functionality. +* After you create the pull request, all status checks must be pass before a maintainer reviews your contribution. + +### Description of the Change + + + +### Alternate Designs + + + +### Possible Drawbacks + + + +### Verification Process + + + +### Release Notes + + + + +### Branch Acknowledgement +[ ] I am acknowledging that I am opening this branch against `staging` diff --git a/.github/PULL_REQUEST_TEMPLATE/performance_improvement.md b/.github/PULL_REQUEST_TEMPLATE/performance_improvement.md new file mode 100644 index 0000000000..0419f9f88d --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/performance_improvement.md @@ -0,0 +1,59 @@ +### Requirements for Contributing a Performance Improvement + +* Fill out the template below. Any pull request that does not include enough information to be reviewed in a timely manner may be closed at the maintainers' discretion. +* The pull request must only affect performance of existing functionality +* After you create the pull request, all status checks must be pass before a maintainer reviews your contribution. + +### Description of the Change + + + +### Quantitative Performance Benefits + + + +### Possible Drawbacks + + + +### Verification Process + + + +### Applicable Issues + + + +### Release Notes + + + + +### Branch Acknowledgement +[ ] I am acknowledging that I am opening this branch against `staging` diff --git a/.github/auto_assign.yml b/.github/auto_assign.yml new file mode 100644 index 0000000000..900e2ceb85 --- /dev/null +++ b/.github/auto_assign.yml @@ -0,0 +1,7 @@ +addReviewers: true + +# A list of team slugs to add as assignees +reviewers: + - opentensor/cortex + +numberOfReviewers: 0 \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000..9ffce36ee7 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,7 @@ +version: 2 +updates: + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "daily" + open-pull-requests-limit: 0 # Only security updates will be opened as PRs diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000000..a58c86f015 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,10 @@ +Welcome! + +Due to [GitHub limitations](https://github.com/orgs/community/discussions/4620), +please switch to **Preview** for links to render properly. + +Please choose the right template for your pull request: + +- 🐛 Are you fixing a bug? [Bug fix](?expand=1&template=bug_fix.md) +- 📈 Are you improving performance? [Performance improvement](?expand=1&template=performance_improvement.md) +- 💻 Are you changing functionality? [Feature change](?expand=1&template=feature_change.md) diff --git a/.github/workflows/_run-e2e-single.yaml b/.github/workflows/_run-e2e-single.yaml new file mode 100644 index 0000000000..60bf153065 --- /dev/null +++ b/.github/workflows/_run-e2e-single.yaml @@ -0,0 +1,80 @@ +name: Run Single E2E Test with multiple Python versions + +on: + workflow_call: + inputs: + nodeid: + required: true + type: string + image-name: + required: true + type: string + +jobs: + run-e2e: + name: "${{ matrix.python-version }}" + runs-on: ubuntu-latest + timeout-minutes: 45 + + strategy: + fail-fast: false + matrix: + python-version: ${{ fromJson(vars.SDK_SUPPORTED_PYTHON_VERSIONS) }} + + steps: + - name: Check-out repository + uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install uv + uses: astral-sh/setup-uv@v4 + with: + enable-cache: false + + - name: Cache uv and venv + uses: actions/cache@v4 + with: + path: | + ~/.cache/uv + .venv + key: uv-${{ runner.os }}-py${{ matrix.python-version }}-${{ hashFiles('pyproject.toml') }} + restore-keys: uv-${{ runner.os }}-py${{ matrix.python-version }}- + + - name: Install dependencies + run: uv sync --extra dev --dev + + - name: Download Cached Docker Image + uses: actions/download-artifact@v4 + with: + name: subtensor-localnet + + - name: Load Docker Image + run: docker load -i subtensor-localnet.tar + + - name: Run test with retry + env: + PY_VERSION: ${{ matrix.python-version }} + LOCALNET_IMAGE_NAME: ${{ inputs.image-name }} + run: | + for i in 1 2 3; do + echo "::group::🔁 Test attempt $i" + if uv run pytest "${{ inputs.nodeid }}" -s; then + echo "✅ Tests passed on attempt $i" + echo "::endgroup::" + exit 0 + else + echo "❌ Tests failed on attempt $i" + echo "::endgroup::" + if [ "$i" -lt 3 ]; then + echo "Retrying..." + sleep 5 + fi + fi + + done + echo "Tests failed after 3 attempts" + exit 1 \ No newline at end of file diff --git a/.github/workflows/changelog-checker.yml b/.github/workflows/changelog-checker.yml new file mode 100644 index 0000000000..aae9580609 --- /dev/null +++ b/.github/workflows/changelog-checker.yml @@ -0,0 +1,24 @@ +name: Changelog guard (for release of hotfix) + +permissions: + contents: read + +on: + pull_request: + branches: + - staging + - master + +jobs: + changelog: + 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 + id: changed + - name: Ensure CHANGELOG.md updated + if: contains(steps.changed.outputs.all_changed_files, 'CHANGELOG.md') == false + uses: actions/github-script@v7 + with: + script: core.setFailed('CHANGELOG.md must be updated.') diff --git a/.github/workflows/compatibility.yml b/.github/workflows/compatibility.yml new file mode 100644 index 0000000000..af001a32ee --- /dev/null +++ b/.github/workflows/compatibility.yml @@ -0,0 +1,27 @@ +name: Requirements compatibility for supported Python versions +permissions: + contents: read + +on: + pull_request: + paths: + - "pyproject.toml" + +jobs: + compatibility: + if: github.event.pull_request.draft == false + runs-on: ubuntu-latest + + strategy: + matrix: + python-version: ${{ fromJson(vars.SDK_SUPPORTED_PYTHON_VERSIONS) }} + + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - run: | + python -m pip install --upgrade pip + python -m pip install ".[dev,cli]" --dry-run --no-deps diff --git a/.github/workflows/docker_release.yml b/.github/workflows/docker_release.yml new file mode 100644 index 0000000000..58f94c0571 --- /dev/null +++ b/.github/workflows/docker_release.yml @@ -0,0 +1,51 @@ +name: Build and Push Docker Image +# https://github.com/sigstore/cosign +on: + workflow_dispatch: + inputs: + tag: + description: 'Docker image tag' + required: true + default: 'latest' + +jobs: + build-and-push: + runs-on: ubuntu-latest + + permissions: + contents: read + id-token: write + + steps: + - name: Check out code + uses: actions/checkout@v4 + + - name: Install cosign + uses: sigstore/cosign-installer@v3 + + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + registry: docker.io + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build and push Docker image + uses: docker/build-push-action@v6 + with: + context: . + push: true + tags: | + opentensorfdn/bittensor:${{ github.event.inputs.tag }} + opentensorfdn/bittensor:latest + provenance: false + + - name: Sign the images with GitHub OIDC Token + env: + DIGEST: ${{ steps.build.outputs.digest }} + TAGS: ${{ steps.build.outputs.tags }} + run: | + echo "${TAGS}" | xargs -I {} cosign sign --yes {}@${DIGEST} diff --git a/.github/workflows/e2e-subtensor-tests.yaml b/.github/workflows/e2e-subtensor-tests.yaml new file mode 100644 index 0000000000..dc9d20dba8 --- /dev/null +++ b/.github/workflows/e2e-subtensor-tests.yaml @@ -0,0 +1,159 @@ +name: E2E Subtensor Tests + +concurrency: + group: e2e-subtensor-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +on: + pull_request: + branches: + - '**' + types: [ opened, synchronize, reopened, ready_for_review, labeled, unlabeled ] + + workflow_dispatch: + inputs: + verbose: + description: "Output more information when triggered manually" + required: false + default: "" + +# job to run tests in parallel +jobs: + # Looking for e2e tests + find-tests: + runs-on: ubuntu-latest + if: ${{ github.event.pull_request.draft == false }} + outputs: + test-files: ${{ steps.get-tests.outputs.test-files }} + steps: + - name: Check-out repository under $GITHUB_WORKSPACE + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.10' + + - name: Install uv + uses: astral-sh/setup-uv@v4 + with: + enable-cache: false + cache-dependency-glob: '**/pyproject.toml' + ignore-nothing-to-cache: true + + - name: Cache uv and venv + uses: actions/cache@v4 + with: + path: | + ~/.cache/uv + .venv + key: uv-${{ runner.os }}-py3.10-${{ hashFiles('pyproject.toml') }} + restore-keys: uv-${{ runner.os }}-py3.10- + + - name: Install dependencies (faster if cache hit) + run: uv sync --extra dev --dev + + - name: Find test files + id: get-tests + shell: bash + run: | + set -euo pipefail + test_matrix=$( + uv run pytest -q --collect-only tests/e2e_tests \ + | sed -n '/^e2e_tests\//p' \ + | sed 's|^|tests/|' \ + | jq -R -s -c ' + split("\n") + | map(select(. != "")) + | map({nodeid: ., label: (sub("^tests/e2e_tests/"; ""))}) + ' + ) + echo "Found tests: $test_matrix" + echo "test-files=$test_matrix" >> "$GITHUB_OUTPUT" + + # Pull docker image + pull-docker-image: + runs-on: ubuntu-latest + outputs: + image-name: ${{ steps.set-image.outputs.image }} + steps: + - name: Set Docker image tag based on label or branch + id: set-image + run: | + echo "Event: $GITHUB_EVENT_NAME" + echo "Branch: $GITHUB_REF_NAME" + + echo "Reading labels ..." + if [[ "${GITHUB_EVENT_NAME}" == "pull_request" ]]; then + labels=$(jq -r '.pull_request.labels[].name' "$GITHUB_EVENT_PATH") + else + labels="" + fi + + image="" + + for label in $labels; do + echo "Found label: $label" + case "$label" in + "subtensor-localnet:main") + image="ghcr.io/opentensor/subtensor-localnet:main" + break + ;; + "subtensor-localnet:testnet") + image="ghcr.io/opentensor/subtensor-localnet:testnet" + break + ;; + "subtensor-localnet:devnet") + image="ghcr.io/opentensor/subtensor-localnet:devnet" + break + ;; + esac + done + + if [[ -z "$image" ]]; then + # fallback to default based on branch + if [[ "${GITHUB_REF_NAME}" == "master" ]]; then + image="ghcr.io/opentensor/subtensor-localnet:main" + else + image="ghcr.io/opentensor/subtensor-localnet:devnet-ready" + fi + fi + + echo "✅ Final selected image: $image" + echo "image=$image" >> "$GITHUB_OUTPUT" + + - name: Log in to GitHub Container Registry + run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u $GITHUB_ACTOR --password-stdin + + - name: Pull Docker Image + run: docker pull ${{ steps.set-image.outputs.image }} + + - name: Save Docker Image to Cache + run: docker save -o subtensor-localnet.tar ${{ steps.set-image.outputs.image }} + + - name: Upload Docker Image as Artifact + uses: actions/upload-artifact@v4 + with: + name: subtensor-localnet + path: subtensor-localnet.tar + compression-level: 0 + + # Job to run tests in parallel + # Since GH Actions matrix has a limit of 256 jobs, we need to split the tests into multiple jobs with different + # Python versions. To reduce DRY we use reusable workflow. + + e2e-test: + name: ${{ matrix.label }} + needs: + - find-tests + - pull-docker-image + strategy: + fail-fast: false + max-parallel: 16 + matrix: + include: ${{ fromJson(needs.find-tests.outputs.test-files) }} + uses: ./.github/workflows/_run-e2e-single.yaml + with: + nodeid: ${{ matrix.nodeid }} + image-name: ${{ needs.pull-docker-image.outputs.image-name }} + secrets: inherit diff --git a/.github/workflows/flake8-and-mypy.yml b/.github/workflows/flake8-and-mypy.yml new file mode 100644 index 0000000000..8f30259c04 --- /dev/null +++ b/.github/workflows/flake8-and-mypy.yml @@ -0,0 +1,50 @@ +name: Flake8 and Mypy - linters check +permissions: + contents: read + +on: + pull_request: + types: [opened, synchronize, reopened, edited] + +jobs: + linters: + if: github.event.pull_request.draft == false + runs-on: ubuntu-latest + + strategy: + fail-fast: false + max-parallel: 5 + matrix: + python-version: ${{ fromJson(vars.SDK_SUPPORTED_PYTHON_VERSIONS) }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install uv + uses: astral-sh/setup-uv@v4 + with: + enable-cache: false + + - name: Cache uv and .venv + uses: actions/cache@v4 + with: + path: | + ~/.cache/uv + .venv + key: uv-${{ runner.os }}-${{ runner.arch }}-py${{ matrix.python-version }}-${{ hashFiles('pyproject.toml') }} + restore-keys: uv-${{ runner.os }}-${{ runner.arch }}-py${{ matrix.python-version }}- + + - 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/ \ No newline at end of file diff --git a/.github/workflows/monitor_requirements_size_master.yml b/.github/workflows/monitor_requirements_size_master.yml new file mode 100644 index 0000000000..8315c8190a --- /dev/null +++ b/.github/workflows/monitor_requirements_size_master.yml @@ -0,0 +1,85 @@ +# This workflow measures the disk size of a virtual environment +# after installing the Bittensor SDK across multiple Python versions. +# It runs only when a new pull request targets the master branch, +# and posts a comment with the results. +name: Monitor SDK Requirements Size + +on: + pull_request: + types: [opened, labeled] + branches: [master, staging] + +permissions: + pull-requests: write + contents: read + +jobs: + measure-venv: + if: github.event_name == 'pull_request' && github.base_ref == 'master' || contains( github.event.pull_request.labels.*.name, 'show-venv-size') + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ${{ fromJson(vars.SDK_SUPPORTED_PYTHON_VERSIONS) }} + outputs: + py310: ${{ steps.set-output.outputs.py310 }} + py311: ${{ steps.set-output.outputs.py311 }} + py312: ${{ steps.set-output.outputs.py312 }} + py313: ${{ steps.set-output.outputs.py313 }} + + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Create virtualenv and install + run: | + python -m venv venv + source venv/bin/activate + pip install --upgrade pip + pip install . + + - name: Measure venv size + id: set-output + run: | + SIZE=$(du -sm venv | cut -f1) + VERSION=${{ matrix.python-version }} + echo "Detected size: $SIZE MB for Python $VERSION" + case "$VERSION" in + 3.10) echo "py310=$SIZE" >> $GITHUB_OUTPUT ;; + 3.11) echo "py311=$SIZE" >> $GITHUB_OUTPUT ;; + 3.12) echo "py312=$SIZE" >> $GITHUB_OUTPUT ;; + 3.13) echo "py313=$SIZE" >> $GITHUB_OUTPUT ;; + esac + + comment-on-pr: + needs: measure-venv + runs-on: ubuntu-latest + steps: + - name: Post venv size summary to PR + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const sizes = { + "3.10": "${{ needs.measure-venv.outputs.py310 || 'N/A' }}", + "3.11": "${{ needs.measure-venv.outputs.py311 || 'N/A' }}", + "3.12": "${{ needs.measure-venv.outputs.py312 || 'N/A' }}", + "3.13": "${{ needs.measure-venv.outputs.py313 || 'N/A' }}", + }; + + const body = [ + '**Bittensor SDK virtual environment sizes by Python version:**', + '', + '```' + ] + .concat(Object.entries(sizes).map(([v, s]) => `Python ${v}: ${s} MB`)) + .concat(['```']) + .join('\n'); + + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body + }); diff --git a/.github/workflows/nightly-e2e-tests-subtensor-main.yml b/.github/workflows/nightly-e2e-tests-subtensor-main.yml new file mode 100644 index 0000000000..49b914871a --- /dev/null +++ b/.github/workflows/nightly-e2e-tests-subtensor-main.yml @@ -0,0 +1,419 @@ +name: Nightly E2E Subtensor tests + +permissions: + contents: read + packages: write + +concurrency: + group: e2e-subtensor-${{ github.ref }} + cancel-in-progress: true + +on: + schedule: + - cron: '0 9 * * *' # Run every night at 2:00 PST + + workflow_dispatch: + inputs: + verbose: + description: "Output more information when triggered manually" + required: false + default: "" + +env: + CARGO_TERM_COLOR: always + VERBOSE: ${{ github.event.inputs.verbose }} + +# job to run tests in parallel +jobs: + # Looking for e2e tests + find-tests: + runs-on: ubuntu-latest + if: ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }} + outputs: + test-files: ${{ steps.get-tests.outputs.test-files }} + steps: + - name: Check-out repository under $GITHUB_WORKSPACE + uses: actions/checkout@v4 + + - name: Find test files + id: get-tests + run: | + test_files=$(find tests/e2e_tests -name "test*.py" | jq -R -s -c 'split("\n") | map(select(. != ""))') + # keep it here for future debug + # test_files=$(find tests/e2e_tests -type f -name "test*.py" | grep -E 'test_(hotkeys|staking)\.py$' | jq -R -s -c 'split("\n") | map(select(. != ""))') + echo "Found test files: $test_files" + echo "test-files=$test_files" >> "$GITHUB_OUTPUT" + shell: bash + + # Pull docker images (devnet-ready and main) + pull-docker-images: + runs-on: ubuntu-latest + steps: + - name: Log in to GitHub Container Registry + run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u $GITHUB_ACTOR --password-stdin + + - name: Pull Docker Image + run: | + docker pull ghcr.io/opentensor/subtensor-localnet:main + docker pull ghcr.io/opentensor/subtensor-localnet:devnet-ready + + - name: List pulled images + run: docker images + + - name: Save Docker Images to Cache + run: | + docker save -o subtensor-localnet-main.tar ghcr.io/opentensor/subtensor-localnet:main + 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 + with: + name: subtensor-localnet-main + path: subtensor-localnet-main.tar + + - name: Upload devnet-ready Docker Image as Artifact + uses: actions/upload-artifact@v4 + with: + name: subtensor-localnet-devnet-ready + path: subtensor-localnet-devnet-ready.tar + + # Determine the day for non-fast-blocks run + check-if-non-fast-blocks-run: + runs-on: ubuntu-latest + outputs: + non-fast-blocks-run: ${{ steps.check.outputs.non-fast-blocks-run }} + steps: + - id: check + run: | + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + echo "🔁 Manual trigger detected. Forcing non-fast-blocks-run=true" + echo "non-fast-blocks-run=true" >> "$GITHUB_OUTPUT" + else + day=$(date -u +%u) + echo "Today is weekday $day" + if [ "$day" -ne 6 ]; then + echo "⏭️ Skipping: not Saturday" + echo "non-fast-blocks-run=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + echo "✅ It is Saturday" + echo "non-fast-blocks-run=true" >> "$GITHUB_OUTPUT" + fi + + # Daily run of fast-blocks tests from `bittensor:master` based on `subtensor:main docker` image + run-fast-blocks-e2e-test-master: + name: "FB master: ${{ matrix.test-file }} / Python ${{ matrix.python-version }}" + needs: + - find-tests + - pull-docker-images + runs-on: ubuntu-latest + timeout-minutes: 25 + outputs: + failed: ${{ steps.test-failed.outputs.failed }} + + strategy: + fail-fast: false # Allow other matrix jobs to run even if this job fails + max-parallel: 32 # Set the maximum number of parallel jobs (same as we have cores in ubuntu-latest runner) + matrix: + os: + - ubuntu-latest + test-file: ${{ fromJson(needs.find-tests.outputs.test-files) }} + python-version: ${{ fromJson(vars.SDK_SUPPORTED_PYTHON_VERSIONS) }} + steps: + - name: Check-out repository + uses: actions/checkout@v4 + with: + ref: master + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install uv + uses: astral-sh/setup-uv@v4 + + - name: install dependencies + run: uv sync --extra dev --dev + + - name: Download Cached Docker Image + uses: actions/download-artifact@v4 + with: + name: subtensor-localnet-main + + - name: Load Docker Image + run: docker load -i subtensor-localnet-main.tar + + - name: Run tests with retry + id: test-failed + env: + FAST_BLOCKS: "1" + LOCALNET_IMAGE_NAME: "ghcr.io/opentensor/subtensor-localnet:main" + run: | + set +e + for i in 1 2 3; do + echo "::group::🔁 Test attempt $i" + uv run pytest ${{ matrix.test-file }} -s + status=$? + if [ $status -eq 0 ]; then + echo "✅ Tests passed on attempt $i" + echo "::endgroup::" + echo "failed=false" >> "$GITHUB_OUTPUT" + break + else + echo "❌ Tests failed on attempt $i" + echo "::endgroup::" + if [ $i -eq 3 ]; then + echo "Tests failed after 3 attempts" + echo "failed=true" >> "$GITHUB_OUTPUT" + exit 1 + fi + echo "Retrying..." + sleep 5 + fi + done + + # Daily run of fast-blocks tests from `bittensor:staging` based on `subtensor:devnet-ready` docker image + run-fast-blocks-e2e-test-staging: + name: "FB staging: ${{ matrix.test-file }} / Python ${{ matrix.python-version }}" + needs: + - find-tests + - pull-docker-images + runs-on: ubuntu-latest + timeout-minutes: 25 + outputs: + failed: ${{ steps.test-failed.outputs.failed }} + + strategy: + fail-fast: false # Allow other matrix jobs to run even if this job fails + max-parallel: 32 # Set the maximum number of parallel jobs (same as we have cores in ubuntu-latest runner) + matrix: + os: + - ubuntu-latest + test-file: ${{ fromJson(needs.find-tests.outputs.test-files) }} + python-version: ${{ fromJson(vars.SDK_SUPPORTED_PYTHON_VERSIONS) }} + steps: + - name: Check-out repository + uses: actions/checkout@v4 + with: + ref: staging + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install uv + uses: astral-sh/setup-uv@v4 + + - name: install dependencies + run: uv sync --extra dev --dev + + - name: Download Cached Docker Image + uses: actions/download-artifact@v4 + with: + name: subtensor-localnet-devnet-ready + + - name: Load Docker Image + run: docker load -i subtensor-localnet-devnet-ready.tar + + - name: Run tests with retry + id: test-failed + env: + FAST_BLOCKS: "1" + LOCALNET_IMAGE_NAME: "ghcr.io/opentensor/subtensor-localnet:devnet-ready" + run: | + set +e + for i in 1 2 3; do + echo "::group::🔁 Test attempt $i" + uv run pytest ${{ matrix.test-file }} -s + status=$? + if [ $status -eq 0 ]; then + echo "✅ Tests passed on attempt $i" + echo "::endgroup::" + echo "failed=false" >> "$GITHUB_OUTPUT" + break + else + echo "❌ Tests failed on attempt $i" + echo "::endgroup::" + if [ $i -eq 3 ]; then + echo "Tests failed after 3 attempts" + echo "failed=true" >> "$GITHUB_OUTPUT" + exit 1 + fi + echo "Retrying..." + sleep 5 + fi + done + + # Saturday run of non-fast-blocks tests from `bittensor:master` based on `subtensor:main` docker image + run-non-fast-blocks-e2e-test-master: + if: needs.check-if-non-fast-blocks-run.outputs.non-fast-blocks-run == 'true' + name: "NFB master: ${{ matrix.test-file }} / Python ${{ matrix.python-version }}" + needs: + - check-if-non-fast-blocks-run + - find-tests + - pull-docker-images + runs-on: ubuntu-latest + timeout-minutes: 1440 + outputs: + failed: ${{ steps.test-failed.outputs.failed }} + + strategy: + fail-fast: false # Allow other matrix jobs to run even if this job fails + max-parallel: 32 # Set the maximum number of parallel jobs (same as we have cores in ubuntu-latest runner) + matrix: + os: + - ubuntu-latest + test-file: ${{ fromJson(needs.find-tests.outputs.test-files) }} + python-version: ${{ fromJson(vars.SDK_SUPPORTED_PYTHON_VERSIONS) }} + + steps: + - name: Check-out repository + uses: actions/checkout@v4 + with: + ref: master + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install uv + uses: astral-sh/setup-uv@v4 + + - name: install dependencies + run: uv sync --extra dev --dev + + - name: Download Cached Docker Image + uses: actions/download-artifact@v4 + with: + name: subtensor-localnet-main + + - name: Load Docker Image + run: docker load -i subtensor-localnet-main.tar + + - name: Run patched E2E tests + id: test-failed + env: + FAST_BLOCKS: "0" + LOCALNET_IMAGE_NAME: "ghcr.io/opentensor/subtensor-localnet:main" + run: | + set +e + for i in 1 2 3; do + echo "::group::🔁 Test attempt $i" + uv run pytest ${{ matrix.test-file }} -s + status=$? + if [ $status -eq 0 ]; then + echo "✅ Tests passed on attempt $i" + echo "::endgroup::" + echo "failed=false" >> "$GITHUB_OUTPUT" + break + else + echo "❌ Tests failed on attempt $i" + echo "::endgroup::" + if [ $i -eq 3 ]; then + echo "Tests failed after 3 attempts" + echo "failed=true" >> "$GITHUB_OUTPUT" + exit 1 + fi + echo "Retrying..." + sleep 5 + fi + done + + # Saturday run of non-fast-blocks tests from `bittensor:staging` based on `subtensor:devnet-ready` docker image + run-non-fast-blocks-e2e-test-staging: + if: needs.check-if-non-fast-blocks-run.outputs.non-fast-blocks-run == 'true' + name: "NFB staging: ${{ matrix.test-file }} / Python ${{ matrix.python-version }}" + needs: + - check-if-non-fast-blocks-run + - find-tests + - pull-docker-images + runs-on: ubuntu-latest + timeout-minutes: 1440 + outputs: + failed: ${{ steps.test-failed.outputs.failed }} + + strategy: + fail-fast: false # Allow other matrix jobs to run even if this job fails + max-parallel: 32 # Set the maximum number of parallel jobs (same as we have cores in ubuntu-latest runner) + matrix: + os: + - ubuntu-latest + test-file: ${{ fromJson(needs.find-tests.outputs.test-files) }} + python-version: ${{ fromJson(vars.SDK_SUPPORTED_PYTHON_VERSIONS) }} + + steps: + - name: Check-out repository + uses: actions/checkout@v4 + with: + ref: staging + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install uv + uses: astral-sh/setup-uv@v4 + + - name: install dependencies + run: uv sync --extra dev --dev + + - name: Download Cached Docker Image + uses: actions/download-artifact@v4 + with: + name: subtensor-localnet-devnet-ready + + - name: Load Docker Image + run: docker load -i subtensor-localnet-devnet-ready.tar + + - name: Run patched E2E tests + id: test-failed + env: + FAST_BLOCKS: "0" + LOCALNET_IMAGE_NAME: "ghcr.io/opentensor/subtensor-localnet:devnet-ready" + run: | + set +e + for i in 1 2 3; do + echo "::group::🔁 Test attempt $i" + uv run pytest ${{ matrix.test-file }} -s + status=$? + if [ $status -eq 0 ]; then + echo "✅ Tests passed on attempt $i" + echo "::endgroup::" + echo "failed=false" >> "$GITHUB_OUTPUT" + break + else + echo "❌ Tests failed on attempt $i" + echo "::endgroup::" + if [ $i -eq 3 ]; then + echo "Tests failed after 3 attempts" + echo "failed=true" >> "$GITHUB_OUTPUT" + exit 1 + fi + echo "Retrying..." + sleep 5 + fi + done + + # Send centralized Discord failure notification + notify-on-failure: + needs: + - run-fast-blocks-e2e-test-master + - run-fast-blocks-e2e-test-staging + - run-non-fast-blocks-e2e-test-master + - run-non-fast-blocks-e2e-test-staging + if: | + needs.run-fast-blocks-e2e-test-master.outputs.failed == 'true' || + needs.run-fast-blocks-e2e-test-staging.outputs.failed == 'true' || + needs.run-non-fast-blocks-e2e-test-master.outputs.failed == 'true' || + needs.run-non-fast-blocks-e2e-test-staging.outputs.failed == 'true' + runs-on: ubuntu-latest + steps: + - name: Send centralized Discord failure notification + run: | + curl -X POST -H "Content-Type: application/json" \ + -d "{\"username\": \"Nightly CI\", \"content\": \"❌ Nightly E2E tests failed. Check run: <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}>\"}" \ + "${{ secrets.NIGHTLY_WEBHOOK_URL }}" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000000..f5a988b42a --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,72 @@ +name: Build and Publish Python Package + +on: + workflow_dispatch: + inputs: + version: + description: 'Version to release' + required: true + type: string + +jobs: + build: + name: Build Python distribution + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.11' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install setuptools wheel twine build toml + + - name: Build package + run: python -m build --sdist --wheel --outdir dist/ + + - name: Check if package version already exists + run: | + PACKAGE_NAME=$(python -c "import toml; print(toml.load('pyproject.toml')['project']['name'])") + PACKAGE_VERSION=${{ github.event.inputs.version }} + if twine check dist/*; then + if pip install $PACKAGE_NAME==$PACKAGE_VERSION; then + echo "Error: Version $PACKAGE_VERSION of $PACKAGE_NAME already exists on PyPI" + exit 1 + else + echo "Version $PACKAGE_VERSION of $PACKAGE_NAME does not exist on PyPI. Proceeding with upload." + fi + else + echo "Error: Twine check failed." + exit 1 + fi + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: dist + path: dist/ + + approve-and-publish: + needs: build + runs-on: ubuntu-latest + environment: release + permissions: + contents: read + id-token: write + + steps: + - name: Download artifact + uses: actions/download-artifact@v4 + with: + name: dist + path: dist/ + + - name: Publish package distributions to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + verbose: true + print-hash: true diff --git a/.github/workflows/ruff.yml b/.github/workflows/ruff.yml new file mode 100644 index 0000000000..7dfaf2eda5 --- /dev/null +++ b/.github/workflows/ruff.yml @@ -0,0 +1,33 @@ +name: Ruff - formatter check +permissions: + contents: read + +on: + pull_request: + types: [opened, synchronize, reopened, edited] + +jobs: + ruff: + if: github.event.pull_request.draft == false + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + 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 + + - name: Ruff format check + run: | + source venv/bin/activate + python -m ruff format --diff bittensor diff --git a/.github/workflows/unit-and-integration-tests.yml b/.github/workflows/unit-and-integration-tests.yml new file mode 100644 index 0000000000..3e2338facd --- /dev/null +++ b/.github/workflows/unit-and-integration-tests.yml @@ -0,0 +1,59 @@ +name: Unit and integration tests checker +permissions: + contents: read + +on: + pull_request: + types: [opened, synchronize, reopened, edited] + +jobs: + unit-and-integration-tests: + if: github.event.pull_request.draft == false + runs-on: ubuntu-latest + + strategy: + fail-fast: false + max-parallel: 5 + matrix: + python-version: ${{ fromJson(vars.SDK_SUPPORTED_PYTHON_VERSIONS) }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install uv + uses: astral-sh/setup-uv@v4 + with: + enable-cache: false + + - name: Cache uv and .venv + uses: actions/cache@v4 + with: + path: | + ~/.cache/uv + .venv + key: uv-${{ runner.os }}-${{ runner.arch }}-py${{ matrix.python-version }}-${{ hashFiles('pyproject.toml') }} + restore-keys: | + uv-${{ runner.os }}-${{ runner.arch }}-py${{ matrix.python-version }}- + + - name: Sync dev deps (idempotent; fast on cache hit) + run: uv sync --extra dev --dev + + - name: Unit tests + timeout-minutes: 20 + env: + PYTHONUNBUFFERED: "1" + run: | + uv run pytest -n 2 tests/unit_tests/ --reruns 3 + + - name: Integration tests + timeout-minutes: 20 + env: + PYTHONUNBUFFERED: "1" + run: | + uv run pytest -n 2 tests/integration_tests/ --reruns 3 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 8be33d1467..5cc3b79913 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,20 @@ # Byte-compiled / optimized / DLL files -__pycache__/ +**/__pycache__/ *.py[cod] *$py.class *.pyc +# Remove notebooks. +*.ipynb + +# weigths and biases +wandb/ + +*.csv +*.torch +*.pt +*.log + # runs/data/models/logs/~ data/ **/data/ @@ -187,11 +198,7 @@ yarn-error.log* **/env/* **/data/* **/.data/* -**/*.txt **/tmp/* -# Ignore any changes in protobuf code, since that should be locally generated -**/bittensor_pb2.py -**/bittensor_pb2_grpc.py **/.bash_history **/*.xml @@ -201,3 +208,9 @@ yarn-error.log* # Replicate library **/.replicate replicate.yaml +**/run.sh + +# Notebooks +*.ipynb + +tests/zombienet/bin/**/* \ No newline at end of file diff --git a/.test_durations b/.test_durations new file mode 100644 index 0000000000..8cb7d74bff --- /dev/null +++ b/.test_durations @@ -0,0 +1,268 @@ +{ + "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_delegate_stake": 32.565206749999994, + "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_inspect": 2.0870491260000037, + "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_metagraph": 17.437785333, + "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_neuron_run_reregister_false": 35.75446520799999, + "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_nominate": 38.171487959, + "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_overview": 54.78253583300001, + "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_overview_all": 303.709275458, + "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_overview_no_wallet": 33.569985001, + "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_overview_not_in_first_subnet": 7.832046707999993, + "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_overview_with_hotkeys_config": 1.235335959000004, + "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_overview_with_sort_by_bad_column_name": 34.20312183400001, + "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_overview_with_sort_by_config": 1.4365408759999951, + "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_overview_with_sort_order_config": 1.4505757079999952, + "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_overview_with_sort_order_config_bad_sort_type": 34.18927604199999, + "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_overview_with_width_config": 1.6561556670000002, + "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_overview_without_hotkeys_config": 1.2479347909999987, + "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_overview_without_sort_by_config": 34.193473041, + "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_overview_without_sort_order_config": 1.436726291999996, + "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_overview_without_width_config": 1.449721043000011, + "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_recycle_register": 48.5383515, + "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_register": 6.655044251, + "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_set_weights": 0.006143250000008038, + "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_stake": 44.89659891599999, + "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_stake_with_all_hotkeys": 31.83300620899999, + "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_stake_with_exclude_hotkeys_from_all": 0.0015482090000062954, + "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_stake_with_multiple_hotkeys_max_stake": 0.0011364169999907858, + "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_stake_with_multiple_hotkeys_max_stake_not_enough_balance": 0.0009022089999959348, + "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_stake_with_single_hotkey_max_stake": 0.0009031669999970404, + "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_stake_with_single_hotkey_max_stake_enough_stake": 0.0012163340000057588, + "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_stake_with_single_hotkey_max_stake_not_enough_balance": 0.0009654589999996688, + "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_stake_with_specific_hotkeys": 357.5746072910001, + "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_transfer": 16.976931332999996, + "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_transfer_not_enough_balance": 22.429711792000006, + "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_undelegate_stake": 27.56590779199999, + "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_unstake_with_all_hotkeys": 38.311913373, + "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_unstake_with_exclude_hotkeys_from_all": 0.0018990010000123903, + "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_unstake_with_multiple_hotkeys_max_stake": 0.0010086670000006848, + "tests/integration_tests/test_cli.py::TestCLIWithNetworkAndConfig::test_unstake_with_specific_hotkeys": 0.0012716660000009483, + "tests/integration_tests/test_cli.py::TestCLIWithNetworkUsingArgs::test_delegate": 0.0012134169999740152, + "tests/integration_tests/test_cli.py::TestCLIWithNetworkUsingArgs::test_list_delegates": 12.917025874999979, + "tests/integration_tests/test_cli.py::TestCLIWithNetworkUsingArgs::test_list_subnets": 0.32005762600000764, + "tests/integration_tests/test_cli.py::TestCLIWithNetworkUsingArgs::test_run_reregister_false": 2.500768667000017, + "tests/integration_tests/test_cli.py::TestCLIWithNetworkUsingArgs::test_run_synapse_all": 8.177792832999984, + "tests/integration_tests/test_cli_no_network.py::TestCLINoNetwork::test_btcli_help": 0.05371037599999795, + "tests/integration_tests/test_cli_no_network.py::TestCLINoNetwork::test_check_configs": 0.5839849989999948, + "tests/integration_tests/test_cli_no_network.py::TestCLINoNetwork::test_list": 0.015767583999995338, + "tests/integration_tests/test_cli_no_network.py::TestCLINoNetwork::test_list_no_wallet": 0.004536540000003697, + "tests/integration_tests/test_cli_no_network.py::TestCLINoNetwork::test_new_coldkey": 0.005761207000013258, + "tests/integration_tests/test_cli_no_network.py::TestCLINoNetwork::test_new_hotkey": 0.003966625999993312, + "tests/integration_tests/test_cli_no_network.py::TestCLINoNetwork::test_regen_coldkey": 0.00497241600000109, + "tests/integration_tests/test_cli_no_network.py::TestCLINoNetwork::test_regen_coldkeypub": 0.00346216599999849, + "tests/integration_tests/test_cli_no_network.py::TestCLINoNetwork::test_regen_hotkey": 0.004310167000014076, + "tests/integration_tests/test_cli_no_network.py::TestCLINoNetwork::test_register_cuda_use_cuda_flag": 2.813618584000004, + "tests/integration_tests/test_dataset.py::test_change_data_size": 9.975283208999997, + "tests/integration_tests/test_dataset.py::test_construct_text_corpus": 5.504439667999989, + "tests/integration_tests/test_dataset.py::test_fail_IPFS_server": 2.991185999999985, + "tests/integration_tests/test_dataset.py::test_mock": 0.11688258300000598, + "tests/integration_tests/test_dataset.py::test_mock_function": 0.11185374999999453, + "tests/integration_tests/test_dataset.py::test_next": 5.809825165999982, + "tests/integration_tests/test_dataset.py::test_text_dataset": 0.003949084000012704, + "tests/integration_tests/test_dendrite.py::test_dendrite_backoff": 0.3834034169999967, + "tests/integration_tests/test_dendrite.py::test_dendrite_forward_tensor": 0.005605251000005751, + "tests/integration_tests/test_dendrite.py::test_dendrite_forward_tensor_endpoint_len_error": 0.0010508339999972804, + "tests/integration_tests/test_dendrite.py::test_dendrite_forward_tensor_endpoint_type_error": 0.0009945420000008198, + "tests/integration_tests/test_dendrite.py::test_dendrite_forward_tensor_input_len_error": 0.0010635420000113527, + "tests/integration_tests/test_dendrite.py::test_dendrite_forward_tensor_mismatch_len_error": 0.0009768319999921005, + "tests/integration_tests/test_dendrite.py::test_dendrite_forward_tensor_shape_error": 0.0010397920000002614, + "tests/integration_tests/test_dendrite.py::test_dendrite_forward_tensor_type_error": 0.0020723339999904056, + "tests/integration_tests/test_dendrite.py::test_dendrite_forward_text": 0.005868083999999385, + "tests/integration_tests/test_dendrite.py::test_dendrite_forward_text_endpoints_tensor": 0.04405566500001612, + "tests/integration_tests/test_dendrite.py::test_dendrite_forward_text_list_string": 0.01698745900000631, + "tests/integration_tests/test_dendrite.py::test_dendrite_forward_text_multiple_endpoints_tensor": 0.01505404200000271, + "tests/integration_tests/test_dendrite.py::test_dendrite_forward_text_multiple_endpoints_tensor_list": 0.01597050000000877, + "tests/integration_tests/test_dendrite.py::test_dendrite_forward_text_non_list": 0.0058105829999988146, + "tests/integration_tests/test_dendrite.py::test_dendrite_forward_text_singular": 0.016635499999992476, + "tests/integration_tests/test_dendrite.py::test_dendrite_forward_text_singular_no_batch_size": 0.01967587499999013, + "tests/integration_tests/test_dendrite.py::test_dendrite_forward_text_singular_string": 0.02379695900000911, + "tests/integration_tests/test_dendrite.py::test_dendrite_forward_text_tensor_list": 0.00768116700000121, + "tests/integration_tests/test_dendrite.py::test_dendrite_forward_text_tensor_list_singular": 0.007751000000013164, + "tests/integration_tests/test_dendrite.py::test_dendrite_to_df": 0.6830525419999987, + "tests/integration_tests/test_dendrite.py::test_failing_synapse": 0.652249334000004, + "tests/integration_tests/test_dendrite.py::test_successful_synapse": 0.5847192090000135, + "tests/integration_tests/test_ipfs.py::test_ipfs_init": 0.005554707999998243, + "tests/integration_tests/test_ipfs.py::test_retrieve_directory": 0.20729179199999237, + "tests/integration_tests/test_keyfile.py::TestKeyFiles::test_create": 0.08020704100000131, + "tests/integration_tests/test_keyfile.py::TestKeyFiles::test_decrypt_keyfile_data_legacy": 3.0671192910000045, + "tests/integration_tests/test_keyfile.py::TestKeyFiles::test_keyfile_mock": 0.018454082999994625, + "tests/integration_tests/test_keyfile.py::TestKeyFiles::test_keyfile_mock_func": 0.019594999999995366, + "tests/integration_tests/test_keyfile.py::TestKeyFiles::test_legacy_coldkey": 0.030612376000000552, + "tests/integration_tests/test_keyfile.py::TestKeyFiles::test_overwriting": 0.031093917000006854, + "tests/integration_tests/test_keyfile.py::TestKeyFiles::test_user_interface": 0.017205207999992922, + "tests/integration_tests/test_keyfile.py::TestKeyFiles::test_validate_password": 0.01777775099999701, + "tests/integration_tests/test_metagraph_integration.py::TestMetagraph::test_full_sync": 3.6405804169999954, + "tests/integration_tests/test_metagraph_integration.py::TestMetagraph::test_lite_sync": 3.6356975829999953, + "tests/integration_tests/test_metagraph_integration.py::TestMetagraph::test_load_sync_save": 3.243659209999997, + "tests/integration_tests/test_metagraph_integration.py::TestMetagraph::test_parameters": 3.0838419149999936, + "tests/integration_tests/test_metagraph_integration.py::TestMetagraph::test_print_empty": 2.6707623749999954, + "tests/integration_tests/test_metagraph_integration.py::TestMetagraph::test_properties": 3.287473416999994, + "tests/integration_tests/test_metagraph_integration.py::TestMetagraph::test_state_dict": 3.296576874000003, + "tests/integration_tests/test_metagraph_integration.py::TestMetagraph::test_sync_block_0": 4.055834208, + "tests/integration_tests/test_priority_thread_pool.py::test_priority_thread_pool": 0.002472417000006999, + "tests/integration_tests/test_prometheus.py::TestPrometheus::test_init_prometheus_failed": 1.491444625000014, + "tests/integration_tests/test_prometheus.py::TestPrometheus::test_init_prometheus_success": 1.6381353319999903, + "tests/integration_tests/test_subtensor_integration.py::TestSubtensor::test_get_balance": 2.5954937909999956, + "tests/integration_tests/test_subtensor_integration.py::TestSubtensor::test_get_balances": 1.9654992910000004, + "tests/integration_tests/test_subtensor_integration.py::TestSubtensor::test_get_current_block": 0.3812910839999972, + "tests/integration_tests/test_subtensor_integration.py::TestSubtensor::test_get_uid_by_hotkey_on_subnet": 0.6584294999999969, + "tests/integration_tests/test_subtensor_integration.py::TestSubtensor::test_hotkey_register": 0.46409241699998915, + "tests/integration_tests/test_subtensor_integration.py::TestSubtensor::test_hotkey_register_failed": 0.3542701670000099, + "tests/integration_tests/test_subtensor_integration.py::TestSubtensor::test_network_overrides": 0.953627209000004, + "tests/integration_tests/test_subtensor_integration.py::TestSubtensor::test_registration_failed": 1.788183917000012, + "tests/integration_tests/test_subtensor_integration.py::TestSubtensor::test_registration_multiprocessed_already_registered": 0.9777173749999974, + "tests/integration_tests/test_subtensor_integration.py::TestSubtensor::test_registration_partly_failed": 1.5698486670000023, + "tests/integration_tests/test_subtensor_integration.py::TestSubtensor::test_registration_stale_then_continue": 0.781868541999998, + "tests/integration_tests/test_subtensor_integration.py::TestSubtensor::test_set_weights": 0.6006925410000008, + "tests/integration_tests/test_subtensor_integration.py::TestSubtensor::test_set_weights_failed": 0.3889112079999961, + "tests/integration_tests/test_subtensor_integration.py::TestSubtensor::test_set_weights_inclusion": 0.4296055830000114, + "tests/integration_tests/test_subtensor_integration.py::TestSubtensor::test_stake": 0.1843938319999836, + "tests/integration_tests/test_subtensor_integration.py::TestSubtensor::test_stake_failed": 0.3917970010000005, + "tests/integration_tests/test_subtensor_integration.py::TestSubtensor::test_stake_inclusion": 0.38589883299999883, + "tests/integration_tests/test_subtensor_integration.py::TestSubtensor::test_transfer": 2.0724527499999965, + "tests/integration_tests/test_subtensor_integration.py::TestSubtensor::test_transfer_dest_as_bytes": 1.2727416259999842, + "tests/integration_tests/test_subtensor_integration.py::TestSubtensor::test_transfer_failed": 1.2812408760000125, + "tests/integration_tests/test_subtensor_integration.py::TestSubtensor::test_transfer_inclusion": 1.2405266240000117, + "tests/integration_tests/test_subtensor_integration.py::TestSubtensor::test_transfer_invalid_dest": 0.4117500419999942, + "tests/integration_tests/test_subtensor_integration.py::TestSubtensor::test_unstake": 0.4006357079999958, + "tests/integration_tests/test_subtensor_integration.py::TestSubtensor::test_unstake_failed": 0.4873798340000093, + "tests/integration_tests/test_subtensor_integration.py::TestSubtensor::test_unstake_inclusion": 0.3860250829999927, + "tests/unit_tests/bittensor_tests/test_axon.py::TestExternalAxon::test_external_ip_not_set_dont_use_internal_ip": 0.006879416000003857, + "tests/unit_tests/bittensor_tests/test_axon.py::TestExternalAxon::test_external_ip_port_set_full_address_internal": 0.004500209000006805, + "tests/unit_tests/bittensor_tests/test_axon.py::TestExternalAxon::test_external_ip_set_full_address_internal": 0.08792841500000037, + "tests/unit_tests/bittensor_tests/test_axon.py::TestExternalAxon::test_external_port_not_set_use_internal_port": 0.004651376000000873, + "tests/unit_tests/bittensor_tests/test_axon.py::TestExternalAxon::test_external_port_set_full_address_internal": 0.00591749999999891, + "tests/unit_tests/bittensor_tests/test_axon.py::test_axon_is_destroyed": 0.040033332000000144, + "tests/unit_tests/bittensor_tests/test_axon.py::test_backward_causal_lm_next_shape_error": 0.0009744579999990677, + "tests/unit_tests/bittensor_tests/test_axon.py::test_backward_causal_lm_shape_error": 0.001580541999999241, + "tests/unit_tests/bittensor_tests/test_axon.py::test_backward_deserialization_error": 0.0005970819999987498, + "tests/unit_tests/bittensor_tests/test_axon.py::test_backward_grads_shape_error": 0.001092959000000171, + "tests/unit_tests/bittensor_tests/test_axon.py::test_backward_invalid_request": 0.0007582499999996273, + "tests/unit_tests/bittensor_tests/test_axon.py::test_backward_last_hidden_shape_error": 0.0008626240000007002, + "tests/unit_tests/bittensor_tests/test_axon.py::test_backward_response_exception": 0.0010987509999997869, + "tests/unit_tests/bittensor_tests/test_axon.py::test_backward_response_success_causal_lm": 0.0032578749999991885, + "tests/unit_tests/bittensor_tests/test_axon.py::test_backward_response_success_causal_lm_next": 0.002431750000001287, + "tests/unit_tests/bittensor_tests/test_axon.py::test_backward_response_success_hidden": 0.001287251000000822, + "tests/unit_tests/bittensor_tests/test_axon.py::test_backward_response_success_text_priority": 0.0034178330000074197, + "tests/unit_tests/bittensor_tests/test_axon.py::test_backward_response_timeout": 0.0009528730000010199, + "tests/unit_tests/bittensor_tests/test_axon.py::test_backward_seq_2_seq_shape_error": 0.0010720409999995795, + "tests/unit_tests/bittensor_tests/test_axon.py::test_forward_batch_shape_error": 0.0007811660000003329, + "tests/unit_tests/bittensor_tests/test_axon.py::test_forward_causal_lm_next_state_exception": 0.0009985000000014566, + "tests/unit_tests/bittensor_tests/test_axon.py::test_forward_causal_lm_state_exception": 0.002173708000000829, + "tests/unit_tests/bittensor_tests/test_axon.py::test_forward_causallm_shape_error": 0.0006132079999998652, + "tests/unit_tests/bittensor_tests/test_axon.py::test_forward_causallm_success": 0.019581957999998956, + "tests/unit_tests/bittensor_tests/test_axon.py::test_forward_causallmnext_shape_error": 0.0007552919999991303, + "tests/unit_tests/bittensor_tests/test_axon.py::test_forward_causallmnext_success": 0.022651415999999536, + "tests/unit_tests/bittensor_tests/test_axon.py::test_forward_deserialization_empty": 0.0009227910000007, + "tests/unit_tests/bittensor_tests/test_axon.py::test_forward_deserialization_error": 0.0008193749999989564, + "tests/unit_tests/bittensor_tests/test_axon.py::test_forward_empty_request": 0.0011124170000007538, + "tests/unit_tests/bittensor_tests/test_axon.py::test_forward_joint_faulty_synapse": 0.01353250000000017, + "tests/unit_tests/bittensor_tests/test_axon.py::test_forward_joint_missing_synapse": 0.013988917000000711, + "tests/unit_tests/bittensor_tests/test_axon.py::test_forward_joint_success": 0.0509341249999995, + "tests/unit_tests/bittensor_tests/test_axon.py::test_forward_last_hidden_shape_error": 0.0008222500000005795, + "tests/unit_tests/bittensor_tests/test_axon.py::test_forward_last_hidden_state_exception": 0.0009832080000000687, + "tests/unit_tests/bittensor_tests/test_axon.py::test_forward_last_hidden_success": 0.0017997490000007943, + "tests/unit_tests/bittensor_tests/test_axon.py::test_forward_not_implemented": 0.001580126000000348, + "tests/unit_tests/bittensor_tests/test_axon.py::test_forward_priority_2nd_request_timeout": 2.009712416999996, + "tests/unit_tests/bittensor_tests/test_axon.py::test_forward_priority_timeout": 27.006205707000003, + "tests/unit_tests/bittensor_tests/test_axon.py::test_forward_response_deserialization_error": 0.0009404579999996443, + "tests/unit_tests/bittensor_tests/test_axon.py::test_forward_seq_2_seq_shape_error": 0.0009308739999998039, + "tests/unit_tests/bittensor_tests/test_axon.py::test_forward_seq_2_seq_state_exception": 0.0013031659999995782, + "tests/unit_tests/bittensor_tests/test_axon.py::test_forward_seq_2_seq_success": 0.0018539589999990724, + "tests/unit_tests/bittensor_tests/test_axon.py::test_forward_seq_shape_error": 0.0008392500000002912, + "tests/unit_tests/bittensor_tests/test_axon.py::test_forward_tensor_success_priority": 0.07963441700000029, + "tests/unit_tests/bittensor_tests/test_axon.py::test_forward_timeout": 0.0021218760000003556, + "tests/unit_tests/bittensor_tests/test_axon.py::test_forward_unknown_error": 0.000990500999999533, + "tests/unit_tests/bittensor_tests/test_axon.py::test_grpc_backward_fails": 0.006330292000001236, + "tests/unit_tests/bittensor_tests/test_axon.py::test_grpc_backward_works": 0.012263416000003247, + "tests/unit_tests/bittensor_tests/test_axon.py::test_grpc_forward_fails": 0.004834957999989342, + "tests/unit_tests/bittensor_tests/test_axon.py::test_grpc_forward_works": 0.015886249999994106, + "tests/unit_tests/bittensor_tests/test_axon.py::test_sign_v2": 0.0025120420000011023, + "tests/unit_tests/bittensor_tests/test_balance.py::TestBalance::test_balance_add": 0.18219254200000012, + "tests/unit_tests/bittensor_tests/test_balance.py::TestBalance::test_balance_add_invalid_type": 0.12365654300000006, + "tests/unit_tests/bittensor_tests/test_balance.py::TestBalance::test_balance_add_other_not_balance": 0.14650508300000098, + "tests/unit_tests/bittensor_tests/test_balance.py::TestBalance::test_balance_div_invalid_type": 0.12069516600000174, + "tests/unit_tests/bittensor_tests/test_balance.py::TestBalance::test_balance_eq_invalid_type": 0.1321914169999996, + "tests/unit_tests/bittensor_tests/test_balance.py::TestBalance::test_balance_eq_other_not_balance": 0.13415275000000015, + "tests/unit_tests/bittensor_tests/test_balance.py::TestBalance::test_balance_floordiv": 0.2226764569999995, + "tests/unit_tests/bittensor_tests/test_balance.py::TestBalance::test_balance_floordiv_other_not_balance": 0.23913508399999994, + "tests/unit_tests/bittensor_tests/test_balance.py::TestBalance::test_balance_init": 0.12514987600000005, + "tests/unit_tests/bittensor_tests/test_balance.py::TestBalance::test_balance_init_from_invalid_value": 0.0004109170000008433, + "tests/unit_tests/bittensor_tests/test_balance.py::TestBalance::test_balance_mul": 0.19085399900000066, + "tests/unit_tests/bittensor_tests/test_balance.py::TestBalance::test_balance_mul_invalid_type": 0.16508675100000048, + "tests/unit_tests/bittensor_tests/test_balance.py::TestBalance::test_balance_mul_other_not_balance": 0.2507777079999993, + "tests/unit_tests/bittensor_tests/test_balance.py::TestBalance::test_balance_neq_none": 0.12535729200000034, + "tests/unit_tests/bittensor_tests/test_balance.py::TestBalance::test_balance_not_eq_none": 0.14622908400000068, + "tests/unit_tests/bittensor_tests/test_balance.py::TestBalance::test_balance_radd_other_not_balance": 0.1727647920000006, + "tests/unit_tests/bittensor_tests/test_balance.py::TestBalance::test_balance_rfloordiv_other_not_balance": 0.21285375000000073, + "tests/unit_tests/bittensor_tests/test_balance.py::TestBalance::test_balance_rmul_other_not_balance": 0.17940537499999998, + "tests/unit_tests/bittensor_tests/test_balance.py::TestBalance::test_balance_rsub_other_not_balance": 0.19510154200000063, + "tests/unit_tests/bittensor_tests/test_balance.py::TestBalance::test_balance_rtruediv_other_not_balance": 0.32300358299999843, + "tests/unit_tests/bittensor_tests/test_balance.py::TestBalance::test_balance_sub": 0.20487529099999868, + "tests/unit_tests/bittensor_tests/test_balance.py::TestBalance::test_balance_sub_invalid_type": 0.13107362499999908, + "tests/unit_tests/bittensor_tests/test_balance.py::TestBalance::test_balance_sub_other_not_balance": 0.20876896000000222, + "tests/unit_tests/bittensor_tests/test_balance.py::TestBalance::test_balance_truediv": 0.20615204100000106, + "tests/unit_tests/bittensor_tests/test_balance.py::TestBalance::test_balance_truediv_other_not_balance": 0.20203299999999835, + "tests/unit_tests/bittensor_tests/test_config.py::test_loaded_config": 0.000341875000000158, + "tests/unit_tests/bittensor_tests/test_config.py::test_prefix": 1.4881067080000019, + "tests/unit_tests/bittensor_tests/test_config.py::test_strict": 0.003527500000000572, + "tests/unit_tests/bittensor_tests/test_config.py::test_to_defaults": 0.0006572089999998809, + "tests/unit_tests/bittensor_tests/test_endpoint.py::test_create_endpoint": 0.0035975830000012365, + "tests/unit_tests/bittensor_tests/test_endpoint.py::test_endpoint_fails_checks": 0.0009294989999997227, + "tests/unit_tests/bittensor_tests/test_endpoint.py::test_endpoint_to_tensor": 0.0014645410000007075, + "tests/unit_tests/bittensor_tests/test_endpoint.py::test_thrash_equality_of_endpoint": 0.5774439579999999, + "tests/unit_tests/bittensor_tests/test_forward_backward.py::test_axon_receptor_forward_works": 0.0101347909999987, + "tests/unit_tests/bittensor_tests/test_forward_backward.py::test_dendrite_backward": 0.01403204099999833, + "tests/unit_tests/bittensor_tests/test_forward_backward.py::test_dendrite_backward_large": 0.0014666259999991382, + "tests/unit_tests/bittensor_tests/test_forward_backward.py::test_dendrite_backward_multiple": 0.0015117080000006666, + "tests/unit_tests/bittensor_tests/test_forward_backward.py::test_dendrite_backward_no_grad": 0.001954291000000552, + "tests/unit_tests/bittensor_tests/test_forward_backward.py::test_dendrite_call_time": 0.029393998999999837, + "tests/unit_tests/bittensor_tests/test_forward_backward.py::test_dendrite_del": 0.0004828739999975795, + "tests/unit_tests/bittensor_tests/test_forward_backward.py::test_dendrite_forward_causal_lm_next_shape_error": 0.00045083400000045515, + "tests/unit_tests/bittensor_tests/test_forward_backward.py::test_dendrite_forward_causal_lm_shape_error": 0.0004375410000001523, + "tests/unit_tests/bittensor_tests/test_forward_backward.py::test_dendrite_forward_last_hidden_shape_error": 0.00042408300000218446, + "tests/unit_tests/bittensor_tests/test_forward_backward.py::test_dendrite_forward_seq_2_seq_shape_error": 0.000591667000000129, + "tests/unit_tests/bittensor_tests/test_forward_backward.py::test_dendrite_forward_tensor_pass_through_text_causal_lm": 0.0019801239999992504, + "tests/unit_tests/bittensor_tests/test_forward_backward.py::test_dendrite_forward_tensor_pass_through_text_causal_lm_next": 0.0015587079999992426, + "tests/unit_tests/bittensor_tests/test_forward_backward.py::test_dendrite_forward_tensor_pass_through_text_last_hidden": 0.0014038749999993883, + "tests/unit_tests/bittensor_tests/test_forward_backward.py::test_dendrite_forward_tensor_pass_through_text_seq_2_seq": 0.0012167919999974686, + "tests/unit_tests/bittensor_tests/test_forward_backward.py::test_dendrite_forward_text_causal_lm": 0.0020301259999992993, + "tests/unit_tests/bittensor_tests/test_forward_backward.py::test_dendrite_forward_text_causal_lm_next": 0.0013322070000008068, + "tests/unit_tests/bittensor_tests/test_forward_backward.py::test_dendrite_forward_text_last_hidden": 0.0011474169999985406, + "tests/unit_tests/bittensor_tests/test_forward_backward.py::test_dendrite_forward_text_seq_2_seq": 0.0011787070000028876, + "tests/unit_tests/bittensor_tests/test_keypair.py::KeyPairTestCase::test_create_ed25519_keypair": 0.001834499999999295, + "tests/unit_tests/bittensor_tests/test_keypair.py::KeyPairTestCase::test_create_keypair_from_private_key": 0.0005444169999986315, + "tests/unit_tests/bittensor_tests/test_keypair.py::KeyPairTestCase::test_create_sr25519_keypair": 0.0015333330000011358, + "tests/unit_tests/bittensor_tests/test_keypair.py::KeyPairTestCase::test_generate_mnemonic": 0.0003291669999967439, + "tests/unit_tests/bittensor_tests/test_keypair.py::KeyPairTestCase::test_hdkd_default_to_dev_mnemonic": 0.0019820840000015494, + "tests/unit_tests/bittensor_tests/test_keypair.py::KeyPairTestCase::test_hdkd_hard_path": 0.0019323339999992584, + "tests/unit_tests/bittensor_tests/test_keypair.py::KeyPairTestCase::test_hdkd_nested_hard_soft_path": 0.0018494169999989651, + "tests/unit_tests/bittensor_tests/test_keypair.py::KeyPairTestCase::test_hdkd_nested_soft_hard_path": 0.0020734170000000773, + "tests/unit_tests/bittensor_tests/test_keypair.py::KeyPairTestCase::test_hdkd_path_gt_32_bytes": 0.001790332999998867, + "tests/unit_tests/bittensor_tests/test_keypair.py::KeyPairTestCase::test_hdkd_soft_path": 0.0016932490000005629, + "tests/unit_tests/bittensor_tests/test_keypair.py::KeyPairTestCase::test_hdkd_unsupported_password": 0.00044658299999866813, + "tests/unit_tests/bittensor_tests/test_keypair.py::KeyPairTestCase::test_incorrect_private_key_length_sr25519": 0.00047804200000101105, + "tests/unit_tests/bittensor_tests/test_keypair.py::KeyPairTestCase::test_incorrect_public_key": 0.0003666670000015415, + "tests/unit_tests/bittensor_tests/test_keypair.py::KeyPairTestCase::test_invalid_mnemic": 0.0004930830000002828, + "tests/unit_tests/bittensor_tests/test_keypair.py::KeyPairTestCase::test_only_provide_public_key": 0.00045920699999868475, + "tests/unit_tests/bittensor_tests/test_keypair.py::KeyPairTestCase::test_only_provide_ss58_address": 0.000522709000001953, + "tests/unit_tests/bittensor_tests/test_keypair.py::KeyPairTestCase::test_provide_no_ss58_address_and_public_key": 0.0005050830000019602, + "tests/unit_tests/bittensor_tests/test_keypair.py::KeyPairTestCase::test_sign_and_verify": 0.0016591679999979903, + "tests/unit_tests/bittensor_tests/test_keypair.py::KeyPairTestCase::test_sign_and_verify_ed25519": 0.0016544579999990816, + "tests/unit_tests/bittensor_tests/test_keypair.py::KeyPairTestCase::test_sign_and_verify_hex_data": 0.001937792000001437, + "tests/unit_tests/bittensor_tests/test_keypair.py::KeyPairTestCase::test_sign_and_verify_incorrect_signature": 0.001960749000000206, + "tests/unit_tests/bittensor_tests/test_keypair.py::KeyPairTestCase::test_sign_and_verify_invalid_message": 0.00183941700000112, + "tests/unit_tests/bittensor_tests/test_keypair.py::KeyPairTestCase::test_sign_and_verify_invalid_signature": 0.0016063319999997105, + "tests/unit_tests/bittensor_tests/test_keypair.py::KeyPairTestCase::test_sign_and_verify_invalid_signature_ed25519": 0.001609873999999678, + "tests/unit_tests/bittensor_tests/test_keypair.py::KeyPairTestCase::test_sign_and_verify_scale_bytes": 0.00196662400000136, + "tests/unit_tests/bittensor_tests/test_keypair.py::KeyPairTestCase::test_sign_missing_private_key": 0.0006992090000004225, + "tests/unit_tests/bittensor_tests/test_keypair.py::KeyPairTestCase::test_sign_unsupported_crypto_type": 0.0004697499999988253, + "tests/unit_tests/bittensor_tests/test_keypair.py::KeyPairTestCase::test_unsupport_crypto_type": 0.0004740830000002916, + "tests/unit_tests/bittensor_tests/test_keypair.py::KeyPairTestCase::test_verify_unsupported_crypto_type": 0.0007947079999990336, + "tests/unit_tests/bittensor_tests/test_metagraph.py::TestMetagraph::test_from_neurons": 0.8742741239999994, + "tests/unit_tests/bittensor_tests/test_neuron.py::TestCoreServer::test_coreserver_reregister_flag_false_exit": 0.006013750000001039, + "tests/unit_tests/bittensor_tests/test_neuron.py::TestCoreServer::test_coreserver_reregister_flag_true": 0.006052874999999958, + "tests/unit_tests/bittensor_tests/test_neuron.py::TestCoreServer::test_model_output_check": 9.921326915999998, + "tests/unit_tests/bittensor_tests/test_neuron.py::TestCoreServer::test_set_fine_tuning_params": 6.299140666000003, + "tests/unit_tests/bittensor_tests/test_neuron.py::TestCoreValidator::test_corevalidator_reregister_flag_false_exit": 0.008880706999999433 +} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000000..4e75d83f4c --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,1862 @@ +# Changelog + +## 9.10.1 /2025-09-05 + +## What's Changed +* Async Get_balances at a specific block returns current block by @basfroman in https://github.com/opentensor/bittensor/pull/3043 +* Fix bug if `block==0` by @basfroman in https://github.com/opentensor/bittensor/pull/3044 +* docs: Update Bittensor documentation link by @Galoretka in https://github.com/opentensor/bittensor/pull/3040 + +## New Contributors +* @Galoretka made their first contribution in https://github.com/opentensor/bittensor/pull/3040 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v9.10.0...v9.10.1 + +## 9.10.0 /2025-08-28 + +## What's Changed +* Fixes broken e2e tests by @thewhaleking in https://github.com/opentensor/bittensor/pull/3020 +* Add async crv4 e2e test by @basfroman in https://github.com/opentensor/bittensor/pull/3022 +* Use `TimelockedWeightCommits` instead of `CRV3WeightCommitsV2` by @basfroman in https://github.com/opentensor/bittensor/pull/3023 +* fix: reflect correct return types for get_delegated by @Arthurdw in https://github.com/opentensor/bittensor/pull/3016 +* Fix `flaky` e2e test (tests.e2e_tests.test_staking.test_safe_staking_scenarios) by @basfroman in https://github.com/opentensor/bittensor/pull/3025 +* Separation of test modules into separate text elements as independent matrix elements by @basfroman in https://github.com/opentensor/bittensor/pull/3027 +* Improve `move_stake` extrinsic (add `move_all_stake` parameter) by @basfroman in https://github.com/opentensor/bittensor/pull/3028 +* Fix tests related with disabled `sudo_set_commit_reveal_weights_enabled` by @basfroman in https://github.com/opentensor/bittensor/pull/3026 + + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v9.9.0...v9.10.0 + +## 9.9.0 /2025-08-11 + +## What's Changed +* Fix crv3 tests after devnet-ready get `CRV3WeightCommitsV2` by @basfroman in https://github.com/opentensor/bittensor/pull/2978 +* Add webhook for failed nightly tests by @basfroman in https://github.com/opentensor/bittensor/pull/2987 +* Fix liquidity test (non-fast-blocks node) by @basfroman in https://github.com/opentensor/bittensor/pull/2988 +* improve nightly logic by @basfroman in https://github.com/opentensor/bittensor/pull/2989 +* improve nightly 2 by @basfroman in https://github.com/opentensor/bittensor/pull/2990 +* Add `get_stake_weight` methods by @basfroman in https://github.com/opentensor/bittensor/pull/2985 +* Handles both exceptions for Swap pallet fetching by @thewhaleking in https://github.com/opentensor/bittensor/pull/2991 +* chore: fix typo by @socialsister in https://github.com/opentensor/bittensor/pull/2969 +* optimisations mostly related to liquidity_list by @thewhaleking in https://github.com/opentensor/bittensor/pull/2980 +* Transfers improvements by @thewhaleking in https://github.com/opentensor/bittensor/pull/2993 +* Remove ownership check in `transfer_stake_extrinsic` and `swap_stake_extrinsic` by @basfroman in https://github.com/opentensor/bittensor/pull/2996 +* Missed await by @thewhaleking in https://github.com/opentensor/bittensor/pull/3002 +* chore: fix typo by @lechpzn in https://github.com/opentensor/bittensor/pull/3001 +* Adds note for installing on macOS by @thewhaleking in https://github.com/opentensor/bittensor/pull/3004 +* Bump bittensor-wallet version by @thewhaleking in https://github.com/opentensor/bittensor/pull/3005 +* Format Error with string docs by @thewhaleking in https://github.com/opentensor/bittensor/pull/3006 +* `LoggingMachine` initialization updated to explicitly call both parent constructors by @basfroman in https://github.com/opentensor/bittensor/pull/3008 +* Fixed `moving_price` conversion from `I96F32` to float by @mcjkula in https://github.com/opentensor/bittensor/pull/3010 +* Add new CRv4 logic by @basfroman in https://github.com/opentensor/bittensor/pull/2999 +* UV Fix by @thewhaleking in https://github.com/opentensor/bittensor/pull/3011 + +## New Contributors +* @socialsister made their first contribution in https://github.com/opentensor/bittensor/pull/2969 +* @lechpzn made their first contribution in https://github.com/opentensor/bittensor/pull/3001 +* @mcjkula made their first contribution in https://github.com/opentensor/bittensor/pull/3010 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v9.8.3...v9.9.0 + +## 9.8.3 /2025-07-18 +* improve make file by @basfroman in https://github.com/opentensor/bittensor/pull/2965 +* Move all workflows from `app.circleci.com` to `GH actions` by @basfroman in https://github.com/opentensor/bittensor/pull/2970 +* Improve `changelog` workflow by @basfroman in https://github.com/opentensor/bittensor/pull/2973 +* Add SECURITY.md by @basfroman in https://github.com/opentensor/bittensor/pull/2976 +* Improve test infrastructure by @basfroman in https://github.com/opentensor/bittensor/pull/2974 +* Add labels checker by @basfroman in https://github.com/opentensor/bittensor/pull/2977 +* Use specified block/hash in metagraph, get_subnet, get_all_subnets by @thewhaleking in https://github.com/opentensor/bittensor/pull/2979 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v9.8.2...v9.8.3 + +## 9.8.2 /2025-07-10 + +## What's Changed +* edit docstrings by @MichaelTrestman in https://github.com/opentensor/bittensor/pull/2951 +* fix and improve e2e stake fee test by @basfroman in https://github.com/opentensor/bittensor/pull/2959 +* Make DynamicInfo backwards compatible by @basfroman in https://github.com/opentensor/bittensor/pull/2958 +* Update staking fee logic by @basfroman in https://github.com/opentensor/bittensor/pull/2961 + +## New Contributors +* @MichaelTrestman made their first contribution in https://github.com/opentensor/bittensor/pull/2951 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v9.8.1...v9.8.2 + +## 9.8.1 /2025-07-08 + +## What's Changed +* New logic to get price for `DynamicInfo` by @basfroman in https://github.com/opentensor/bittensor/pull/2952 +* Update to safe_staking limits by @ibraheem-abe in https://github.com/opentensor/bittensor/pull/2950 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v9.8.0...v9.8.1 + +## 9.8.0 /2025-07-07 + +## What's Changed +* Update e2e-tests (metagraph_info, staking) by @basfroman in https://github.com/opentensor/bittensor/pull/2907 +* Add method to fetch parents for child hotkeys by @thewhaleking in https://github.com/opentensor/bittensor/pull/2906 +* Add local env variable for `config.wallet.*` by @basfroman in https://github.com/opentensor/bittensor/pull/2908 +* Subtensor docstring cleanup by @thewhaleking in https://github.com/opentensor/bittensor/pull/2910 +* chore(ci): upgrade Docker GitHub Actions to latest stable versions by @MamunC0der in https://github.com/opentensor/bittensor/pull/2912 +* improve `scripts/install.sh` by @basfroman in https://github.com/opentensor/bittensor/pull/2914 +* Cleanup websocket integration data by @thewhaleking in https://github.com/opentensor/bittensor/pull/2915 +* chore: fix some typos in comment by @eveneast in https://github.com/opentensor/bittensor/pull/2868 +* Add `SKIP_PULL` variable for conftest.py by @basfroman in https://github.com/opentensor/bittensor/pull/2920 +* Update `SubnetIdentity` with subtensor related v3 changes by @basfroman in https://github.com/opentensor/bittensor/pull/2923 +* add time for flaky test by @basfroman in https://github.com/opentensor/bittensor/pull/2924 +* Add `root_set_pending_childkey_cooldown_extrinsic` by @basfroman in https://github.com/opentensor/bittensor/pull/2925 +* New logic for alpha low and high (0.025 <= alpha_low <= alpha_high <= 1) by @basfroman in https://github.com/opentensor/bittensor/pull/2927 +* Fix Typo in test_commit_weights.py by @zeevick10 in https://github.com/opentensor/bittensor/pull/2922 +* New websockets option by @thewhaleking in https://github.com/opentensor/bittensor/pull/2917 +* Retry archive node support by @thewhaleking in https://github.com/opentensor/bittensor/pull/2909 +* Fix: Safe staking e2e test by @ibraheem-abe in https://github.com/opentensor/bittensor/pull/2929 +* fix type conversion in axon preprocess by @Windfarer in https://github.com/opentensor/bittensor/pull/2930 +* [LINT] Improved type hints by @Arthurdw in https://github.com/opentensor/bittensor/pull/2918 +* Set balance netuids in staking tests by @thewhaleking in https://github.com/opentensor/bittensor/pull/2934 +* Improve subnet method logic by @basfroman in https://github.com/opentensor/bittensor/pull/2937 +* Update `SubnetHyperparameters` with v2 by @basfroman in https://github.com/opentensor/bittensor/pull/2938 +* upgrade numpy by @thewhaleking in https://github.com/opentensor/bittensor/pull/2936 +* decode metadata with empty value by @thewhaleking in https://github.com/opentensor/bittensor/pull/2940 +* Reset bonds by @andreea-popescu-reef in https://github.com/opentensor/bittensor/pull/2876 +* Add new logic for `unstake_all` extrinsics by @basfroman in https://github.com/opentensor/bittensor/pull/2897 +* SelectiveMetagraph back by @basfroman in https://github.com/opentensor/bittensor/pull/2887 +* add unstake_all to SubtensorApi.extrinsics by @basfroman in https://github.com/opentensor/bittensor/pull/2943 +* Integrate Liquidity Provider feature by @basfroman in https://github.com/opentensor/bittensor/pull/2939 +* Fix e2e test metagraph after chain change by @basfroman in https://github.com/opentensor/bittensor/pull/2945 + +## New Contributors +* @MamunC0der made their first contribution in https://github.com/opentensor/bittensor/pull/2912 +* @eveneast made their first contribution in https://github.com/opentensor/bittensor/pull/2868 +* @Windfarer made their first contribution in https://github.com/opentensor/bittensor/pull/2930 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v9.7.0...v9.8.0 + +## 9.7.1 /2025-06-06 + +## What's Changed +* Update e2e-tests (metagraph_info, staking) by @basfroman in https://github.com/opentensor/bittensor/pull/2907 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v9.7.0...v9.7.1 + +## 9.7.0 /2025-05-29 + +## What's Changed +* Add `get_subnet_info` by @basfroman in https://github.com/opentensor/bittensor/pull/2894 +* Fix bug in `get_next_epoch_start_block` by @basfroman in https://github.com/opentensor/bittensor/pull/2899 +* e2e workflow: improve skipping logic (no error when skip the job) by @basfroman in https://github.com/opentensor/bittensor/pull/2898 +* Replace `transfer_allow_death` with `transfer_keep_alive` by @basfroman in https://github.com/opentensor/bittensor/pull/2900 +* Fix broken pull request template links (#2867) by @AgSpades in https://github.com/opentensor/bittensor/pull/2883 +* update pr templates with branch ack by @thewhaleking in https://github.com/opentensor/bittensor/pull/2903 + +## New Contributors +* @AgSpades made their first contribution in https://github.com/opentensor/bittensor/pull/2883 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v9.6.1...v9.7.0 + +## 9.6.1 /2025-05-22 + +## What's Changed +* Release/9.6.0 by @ibraheem-abe in https://github.com/opentensor/bittensor/pull/2882 +* Add stake before check `validator_permit` by @basfroman in https://github.com/opentensor/bittensor/pull/2884 +* Add defaults for endpoint and network from local env by @basfroman in https://github.com/opentensor/bittensor/pull/2886 +* Improve error message by @basfroman in https://github.com/opentensor/bittensor/pull/2888 +* Make `unstake` and `unstake_multiple` for all Alphas more clear by @basfroman in https://github.com/opentensor/bittensor/pull/2885 +* fix publish metadata by @basfroman in https://github.com/opentensor/bittensor/pull/2890 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v9.6.0...v9.6.1 + +## 9.6.0 /2025-05-15 + +* Add manual way to show the size of virtual environments in the PR by @basfroman in https://github.com/opentensor/bittensor/pull/2875 +* Improve `Monitor SDK Requirements Size` workflow by @basfroman in https://github.com/opentensor/bittensor/pull/2878 +* Add subtensor.is_subnet_active method by @basfroman in https://github.com/opentensor/bittensor/pull/2877 +* Using `hotkey` instead of `coldkey` to sign extrinsic in `serve_axon` by @basfroman in https://github.com/opentensor/bittensor/pull/2879 +* Rename argument `fallback_chains` to `fallback_endpoints` by @basfroman in https://github.com/opentensor/bittensor/pull/2880 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v9.5.0...v9.6.0 + +## 9.5.0 /2025-05-12 + +## What's Changed +* Release/9.4.0 by @ibraheem-abe in https://github.com/opentensor/bittensor/pull/2837 +* Update subnet units by @thewhaleking in https://github.com/opentensor/bittensor/pull/2838 +* Add `force_register_neuron` into MockSubtensor by @basfroman in https://github.com/opentensor/bittensor/pull/2839 +* Add `Monitor-Requirements-Size` workflow by @basfroman in https://github.com/opentensor/bittensor/pull/2842 +* Add `SelectiveMetagraph` interface into SDK by @basfroman in https://github.com/opentensor/bittensor/pull/2846 +* Update docs for unstake amount by @thewhaleking in https://github.com/opentensor/bittensor/pull/2845 +* Add one more attempt to e2e tests by @basfroman in https://github.com/opentensor/bittensor/pull/2849 +* Fix typos in test documentation and docstrings by @leopardracer in https://github.com/opentensor/bittensor/pull/2848 +* Add bittensor-drand==0.5.0 by @basfroman in https://github.com/opentensor/bittensor/pull/2835 +* Extend selective metagraph logic by @basfroman in https://github.com/opentensor/bittensor/pull/2852 +* fix: $BASH_ENV loading issue by @defitricks in https://github.com/opentensor/bittensor/pull/2851 +* Update github actions versions due to deprecation by @PixelPil0t1 in https://github.com/opentensor/bittensor/pull/2850 +* Add methods to Async/Subtensor class by @basfroman in https://github.com/opentensor/bittensor/pull/2854 +* Cleanup, refactoring, small fix, improvement by @basfroman in https://github.com/opentensor/bittensor/pull/2856 +* Add `period` argument to extrinsics calls by @basfroman in https://github.com/opentensor/bittensor/pull/2857 +* Add nightly run of e2e tests by @basfroman in https://github.com/opentensor/bittensor/pull/2858 +* `period=16` for fast blocks test by @basfroman in https://github.com/opentensor/bittensor/pull/2859 +* Fix some unittests warnings by @zyzniewski-reef in https://github.com/opentensor/bittensor/pull/2774 +* docs: fix typos in `README.md` by @gap-editor in https://github.com/opentensor/bittensor/pull/2861 +* Introduce `SubtensorApi` interface for SDK by @basfroman in https://github.com/opentensor/bittensor/pull/2862 +* Adds `__all__` to easy_imports.py to get rid of all the #noqa stuff by @thewhaleking in https://github.com/opentensor/bittensor/pull/2863 +* `🥩s` to `staking` by @basfroman in https://github.com/opentensor/bittensor/pull/2864 +* Fix `get_next_epoch_start_block` by @basfroman in https://github.com/opentensor/bittensor/pull/2865 +* docs: fix dead link by @GarmashAlex in https://github.com/opentensor/bittensor/pull/2866 +* Add `non-fast-blocks` e2e tests each Saturday by @basfroman in https://github.com/opentensor/bittensor/pull/2860 +* Selective-metagraph -> MetagraphInfo by @basfroman in https://github.com/opentensor/bittensor/pull/2870 +* Improve e2e tests and fix bug in SubtensorApi by @basfroman in https://github.com/opentensor/bittensor/pull/2869 + +## New Contributors +* @defitricks made their first contribution in https://github.com/opentensor/bittensor/pull/2851 +* @PixelPil0t1 made their first contribution in https://github.com/opentensor/bittensor/pull/2850 +* @gap-editor made their first contribution in https://github.com/opentensor/bittensor/pull/2861 +* @GarmashAlex made their first contribution in https://github.com/opentensor/bittensor/pull/2866 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v9.4.0...v9.5.0 + +## 9.4.0 /2025-04-17 + +## What's Changed +* Release/9.3.0 by @ibraheem-abe in https://github.com/opentensor/bittensor/pull/2805 +* Fix for flaky behavior of `test_incentive`, `test_commit_weights` and `test_set_weights` by @basfroman in https://github.com/opentensor/bittensor/pull/2795 +* Add `get_next_epoch_start_block` method to Async/Subtensor by @basfroman in https://github.com/opentensor/bittensor/pull/2808 +* Adds compatibility for torch 2.6.0+ by @thewhaleking in https://github.com/opentensor/bittensor/pull/2811 +* f Update CONTRIBUTING.md by @Hack666r in https://github.com/opentensor/bittensor/pull/2813 +* docs: replaced discord link with documentation by @sashaphmn in https://github.com/opentensor/bittensor/pull/2809 +* sometimes it's still flaky because the chain returns data with time offset by @basfroman in https://github.com/opentensor/bittensor/pull/2816 +* Remove requirements directory by @thewhaleking in https://github.com/opentensor/bittensor/pull/2812 +* version in one place by @thewhaleking in https://github.com/opentensor/bittensor/pull/2806 +* Update CONTRIBUTING hyperlinks by @thewhaleking in https://github.com/opentensor/bittensor/pull/2820 +* 9.3.1a1: Updates changelog by @ibraheem-abe in https://github.com/opentensor/bittensor/pull/2821 +* Remove cubit as extra by @thewhaleking in https://github.com/opentensor/bittensor/pull/2823 +* fix/roman/get-metagraph-identities by @basfroman in https://github.com/opentensor/bittensor/pull/2826 +* Replace waiting logic for `test_commit_reveal_v3.py` by @basfroman in https://github.com/opentensor/bittensor/pull/2827 +* Add `start_call` extrinsic to the sdk by @basfroman in https://github.com/opentensor/bittensor/pull/2828 +* Add `timelock` module by @basfroman in https://github.com/opentensor/bittensor/pull/2824 +* Fix: raise_error=True in AsyncSubtensor fails to get error_message by @zyzniewski-reef in https://github.com/opentensor/bittensor/pull/2771 +* Fix: async set_subnet_identity_extrinsic doesn't sign the extrinsic by @zyzniewski-reef in https://github.com/opentensor/bittensor/pull/2772 +* Update settings.py by @petryshkaCODE in https://github.com/opentensor/bittensor/pull/2814 +* Bumping ruff version by @basfroman in https://github.com/opentensor/bittensor/pull/2831 +* Fix and improve e2e tests after `start call` new limit in subtensor by @basfroman in https://github.com/opentensor/bittensor/pull/2830 +* Add `bittensor.timelock` module description by @basfroman in https://github.com/opentensor/bittensor/pull/2833 +* fix: Update broken link in `CONTRIBUTING.md` by @cypherpepe in https://github.com/opentensor/bittensor/pull/2818 +* docs: added a new badge by @sashaphmn in https://github.com/opentensor/bittensor/pull/2819 +* Fix AxonInfo initialization in get_mock_neuron function by @VolodymyrBg in https://github.com/opentensor/bittensor/pull/2803 +* Dendrite: log ClientOSError as Debug by @Thykof in https://github.com/opentensor/bittensor/pull/2770 +* remove `rao` endpoint from settings by @basfroman in https://github.com/opentensor/bittensor/pull/2834 + +## New Contributors +* @Hack666r made their first contribution in https://github.com/opentensor/bittensor/pull/2813 +* @petryshkaCODE made their first contribution in https://github.com/opentensor/bittensor/pull/2814 +* @cypherpepe made their first contribution in https://github.com/opentensor/bittensor/pull/2818 +* @VolodymyrBg made their first contribution in https://github.com/opentensor/bittensor/pull/2803 +* @Thykof made their first contribution in https://github.com/opentensor/bittensor/pull/2770 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v9.3.0...v9.4.0 + +## 9.3.0 /2025-04-09 + +## What's Changed +* More E2E tests by @zyzniewski-reef in https://github.com/opentensor/bittensor/pull/2754 +* Fix E2E: fix wait_epoch and next_tempo by @zyzniewski-reef in https://github.com/opentensor/bittensor/pull/2753 +* Add all supported python versions to e2e tests workflow by @basfroman in https://github.com/opentensor/bittensor/pull/2761 +* update docker image name by @basfroman in https://github.com/opentensor/bittensor/pull/2760 +* Add pypi package version checker for `python -m bittensor` by @basfroman in https://github.com/opentensor/bittensor/pull/2762 +* Feat: set_children and get_pending_children methods by @zyzniewski-reef in https://github.com/opentensor/bittensor/pull/2752 +* Add logic for keep docker image up to date by @basfroman in https://github.com/opentensor/bittensor/pull/2765 +* Fix: CI/CD Set up Python version for E2E tests by @zyzniewski-reef in https://github.com/opentensor/bittensor/pull/2767 +* Fix E2E Tests: wait for new nonce by @zyzniewski-reef in https://github.com/opentensor/bittensor/pull/2768 +* Fix e2e `conftest.py` for legacy runner by @basfroman in https://github.com/opentensor/bittensor/pull/2769 +* Fix E2E with devnet-ready by @zyzniewski-reef in https://github.com/opentensor/bittensor/pull/2776 +* Add compatibility check for 3.13 by @thewhaleking in https://github.com/opentensor/bittensor/pull/2779 +* Fix E2E test_dendrite by making sure Alice is Top validator in Subnet by @zyzniewski-reef in https://github.com/opentensor/bittensor/pull/2780 +* Add get_owned_hotkeys to subtensor and async one + tests by @basfroman in https://github.com/opentensor/bittensor/pull/2766 +* Add drand-commitments by @basfroman in https://github.com/opentensor/bittensor/pull/2781 +* Missing f-string format by @zyzniewski-reef in https://github.com/opentensor/bittensor/pull/2785 +* bump version by @basfroman in https://github.com/opentensor/bittensor/pull/2786 +* Improvement and fix for https://github.com/opentensor/bittensor/pull/2781 by @basfroman in https://github.com/opentensor/bittensor/pull/2787 +* Add `stop_existing_test_containers` logic before run e2e test/s by @basfroman in https://github.com/opentensor/bittensor/pull/2790 +* Bump async substrate interface by @thewhaleking in https://github.com/opentensor/bittensor/pull/2788 +* Improve CRv3 functionality by @basfroman in https://github.com/opentensor/bittensor/pull/2791 +* Improve logic in Balance magic methods by @basfroman in https://github.com/opentensor/bittensor/pull/2764 +* Requirements update by @thewhaleking in https://github.com/opentensor/bittensor/pull/2789 +* remove Levenshtein requirement by @thewhaleking in https://github.com/opentensor/bittensor/pull/2802 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v9.2.0...v9.3.0 + +## 9.2.0 /2025-03-18 + +## What's Changed +* Fix E2E test_incentive by waiting till start of the very next epoch by @zyzniewski-reef in https://github.com/opentensor/bittensor/pull/2746 +* New era of e2e Tests Bittensor by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2743 +* Allow installation on Py 3.13 by @thewhaleking in https://github.com/opentensor/bittensor/pull/2756 +* Feat/dynamic stake prices by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2755 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v9.1.0...v9.2.0 + +## 9.1.0 /2025-03-12 + +## What's Changed +* Refactor duplicated unittests code by @zyzniewski-reef in https://github.com/opentensor/bittensor/pull/2724 +* Use uv for circleci by @thewhaleking in https://github.com/opentensor/bittensor/pull/2729 +* Fix E2E test_metagraph_info by @zyzniewski-reef in https://github.com/opentensor/bittensor/pull/2728 +* Tests: deduplicate fake_wallet and correctly create Mock by @zyzniewski-reef in https://github.com/opentensor/bittensor/pull/2730 +* E2E Test: wait cooldown period to check set_children effect by @zyzniewski-reef in https://github.com/opentensor/bittensor/pull/2733 +* Tests: wait for Miner/Validator to fully start by @zyzniewski-reef in https://github.com/opentensor/bittensor/pull/2737 +* All metagraph subtensor methods now use block by @thewhaleking in https://github.com/opentensor/bittensor/pull/2738 +* Tests: increse test_incentive timeout + fix sudo_set_weights_set_rate_limit by @zyzniewski-reef in https://github.com/opentensor/bittensor/pull/2739 +* Feat/safe staking by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2736 +* 9.0.5: Bumps version and changelog by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2741 +* Tests: enable E2E test_batch_operations by @zyzniewski-reef in https://github.com/opentensor/bittensor/pull/2744 +* Fix: burned_register supports root subnet (netuid=0 param) by @zyzniewski-reef in https://github.com/opentensor/bittensor/pull/2732 +* Feat: set_delegate_take by @zyzniewski-reef in https://github.com/opentensor/bittensor/pull/2731 +* Renames rate_threshold -> rate_tolerance by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2745 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v9.0.4...v9.1.0 + +## 9.0.4 /2025-03-06 + +## What's Changed +* Release/9.0.3 by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2712 +* improve `wait_for_node_start` until 20 mins by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2714 +* More E2E tests by @zyzniewski-reef in https://github.com/opentensor/bittensor/pull/2678 +* fix(2715): use ChainIdentity for identities by @zyzniewski-reef in https://github.com/opentensor/bittensor/pull/2718 +* Metagraph use block correctly in `_get_all_stakes_from_chain` by @thewhaleking in https://github.com/opentensor/bittensor/pull/2719 +* Integration tests for async-substrate-interface 1.0.4 compatibility by @thewhaleking in https://github.com/opentensor/bittensor/pull/2720 +* Backmerge main staging 904 by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2721 +* Skip E2E test_children by @zyzniewski-reef in https://github.com/opentensor/bittensor/pull/2723 +* More Subtensor unnitests by @zyzniewski-reef in https://github.com/opentensor/bittensor/pull/2713 +* Change to pyproject.toml by @thewhaleking in https://github.com/opentensor/bittensor/pull/2504 +* Updates test_incentive by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2722 +* Use uv for gh actions by @thewhaleking in https://github.com/opentensor/bittensor/pull/2503 +* Bumps async substrate interface by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2725 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v9.0.3...v9.0.4 + +## 9.0.3 /2025-02-26 + +## What's Changed +* Release/9.0.2 by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2696 +* fix: typos in config test by @EricHasegawa in https://github.com/opentensor/bittensor/pull/2693 +* Removes limits in async + unstake_multiple by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2701 +* Fixes get_all_commitments, adds tests. by @thewhaleking in https://github.com/opentensor/bittensor/pull/2699 +* Use `.value` in e2e test by @thewhaleking in https://github.com/opentensor/bittensor/pull/2700 +* Fix e2e test setup by @thewhaleking in https://github.com/opentensor/bittensor/pull/2681 +* Dendrite `__del__` method fix by @thewhaleking in https://github.com/opentensor/bittensor/pull/2702 +* Fix E2E test_set_weights by @zyzniewski-reef in https://github.com/opentensor/bittensor/pull/2703 +* Updates test incentive by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2688 +* Add `get_timestamp` method by @thewhaleking in https://github.com/opentensor/bittensor/pull/2704 +* fix: async get_delegated by @zyzniewski-reef in https://github.com/opentensor/bittensor/pull/2706 +* Properly mock data_chain class methods by @zyzniewski-reef in https://github.com/opentensor/bittensor/pull/2705 +* Install btcli from install sh by @thewhaleking in https://github.com/opentensor/bittensor/pull/2708 +* Bumps dependencies of async substrate + btwallet by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2710 +* Backmerge/main to staging 902 by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2711 + +## New Contributors +* @EricHasegawa made their first contribution in https://github.com/opentensor/bittensor/pull/2693 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v9.0.2...v9.0.3 + +## 9.0.2 /2025-02-24 + +## What's Changed +* CI: Upgrade rust compiler for E2E tests by @zyzniewski-reef in https://github.com/opentensor/bittensor/pull/2690 +* Break away cli reqs by @thewhaleking in https://github.com/opentensor/bittensor/pull/2692 +* Updates DelegateInfo chain data by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2683 +* Backmerge main to staging 901 by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2689 +* fix: typos in documentation files by @zeevick10 in https://github.com/opentensor/bittensor/pull/2687 +* Removes tx limit in stake_multiple by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2694 + +## New Contributors +* @zeevick10 made their first contribution in https://github.com/opentensor/bittensor/pull/2687 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v9.0.1...v9.0.2 + +## 9.0.1 /2025-02-20 + +## What's Changed +* Release/9.0.0 by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2671 +* fix e2e test by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2673 +* fix e2e test incentive by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2674 +* Add compatibility for read-only systems by @Arthurdw in https://github.com/opentensor/bittensor/pull/2676 +* test: use asynccontextmanager for FastAPI lifespan by @zyzniewski-reef in https://github.com/opentensor/bittensor/pull/2597 +* test(2472): offline unittests by @zyzniewski-reef in https://github.com/opentensor/bittensor/pull/2596 +* Removes redundant assignments in Metagraph by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2680 +* Alpha str formatting by @thewhaleking in https://github.com/opentensor/bittensor/pull/2672 +* Add method for fetching all Neuron Certificates on a Netuid by @thewhaleking in https://github.com/opentensor/bittensor/pull/2677 +* Updates tao_stake in MetagraphInfo by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2682 +* fix(2188): configure uvicorn event loop by @zyzniewski-reef in https://github.com/opentensor/bittensor/pull/2679 +* Refactor AsyncSubtensor aenter logic by @thewhaleking in https://github.com/opentensor/bittensor/pull/2684 +* Backmerge master to staging 900 by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2685 + +## New Contributors +* @Arthurdw made their first contribution in https://github.com/opentensor/bittensor/pull/2676 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v9.0.0...v9.0.1 + +## 9.0.0 /2025-02-13 + +## What's Changed +* Optimisations and tests for Async Sync Subtensor by @thewhaleking in https://github.com/opentensor/bittensor/pull/2569 +* [SDK] Get rid of py-substrate-interface (DO NOT MERGE) by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2565 +* Uses the new async_substrate_interface lib by @thewhaleking in https://github.com/opentensor/bittensor/pull/2572 +* AsyncSubstrateInterface Overhaul (with Sync AsyncSubstrate) by @thewhaleking in https://github.com/opentensor/bittensor/pull/2526 +* [SDK] Small improvements by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2575 +* Sync Subtensor warning by @thewhaleking in https://github.com/opentensor/bittensor/pull/2578 +* [SDK] Fixes types in async by @thewhaleking in https://github.com/opentensor/bittensor/pull/2577 +* Release/8.5.2 by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2584 +* fix: typos in documentation files by @leopardracer in https://github.com/opentensor/bittensor/pull/2580 +* fix(2337): btlogging setLevel by @zyzniewski-reef in https://github.com/opentensor/bittensor/pull/2588 +* perf: don't use 2 threads to create FastAPI server by @zyzniewski-reef in https://github.com/opentensor/bittensor/pull/2592 +* fix(2188): don't allow uvicorn to reconfigure event_loop_policy by @zyzniewski-reef in https://github.com/opentensor/bittensor/pull/2591 +* Fix spelling errors by @Dimitrolito in https://github.com/opentensor/bittensor/pull/2586 +* Make code as beautiful as it has never been before by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2599 +* test: bring back old (sync subtensor) tests and fix them by @zyzniewski-reef in https://github.com/opentensor/bittensor/pull/2600 +* YAASO: Yet Another AsyncSubtensor Overhaul by @thewhaleking in https://github.com/opentensor/bittensor/pull/2579 +* Add alias for `Subtensor.commit` as `set_commitment` by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2606 +* [RAO] Add methods to fetch metagraph data from the chain by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2605 +* Rewrite config.py by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2607 +* Update metagraph class with `rao` stuff by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2611 +* [RAO] fix for unit test + refactoring by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2612 +* fix integration metagraph test by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2613 +* Cleanups, fixes, improvements for rao by @thewhaleking in https://github.com/opentensor/bittensor/pull/2614 +* Adds deprecation notice for non-balance amounts by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2615 +* Staging pre merge port rao (New async substrate) by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2610 +* Tests for SyncSubtensor by @zyzniewski-reef in https://github.com/opentensor/bittensor/pull/2617 +* Many small fixes by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2619 +* Use async-substrate-interface for runtime decoding by @zyzniewski-reef in https://github.com/opentensor/bittensor/pull/2618 +* Pins torch version to 2.5.1 by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2623 +* Fixes, adds stake and other methods by @thewhaleking in https://github.com/opentensor/bittensor/pull/2622 +* Fix typos by @Marcofann in https://github.com/opentensor/bittensor/pull/2620 +* Add `subnet_volume` field to `MetagraphInfo` and `DynamicInfo` by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2624 +* Update wallet creation command logs post-install by @HudsonGraeme in https://github.com/opentensor/bittensor/pull/2621 +* fix subtensor methods for async by @thewhaleking in https://github.com/opentensor/bittensor/pull/2628 +* Subnet burn cost return type by @thewhaleking in https://github.com/opentensor/bittensor/pull/2629 +* Specifies a range of torch versions, rather than a pinned version. by @thewhaleking in https://github.com/opentensor/bittensor/pull/2632 +* Adds subnet registration extrinsic by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2630 +* Bumps btwallet 302 by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2633 +* SKD implementation for Subtensor `Feat/RPC Upgrades`. PR #1205 by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2627 +* Bug fixes after release SDK v9.0.0rc1 by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2637 +* Adds Latent Lite endpoint to the SDK by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2641 +* Bringing meta fields to a common form with float values float(TAO) instead of Balance and Tensor by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2642 +* Adds `get_all_commitments` and fixes commitment tests and `query_map` by @thewhaleking in https://github.com/opentensor/bittensor/pull/2644 +* Fix for extra fields from chain data by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2647 +* Fix InfoBase + dataclasses by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2649 +* fix integration tests by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2651 +* feat: Add logging for unexpected header keys in Synapse by @crStiv in https://github.com/opentensor/bittensor/pull/2587 +* Fixes Dendrite new loop close by @thewhaleking in https://github.com/opentensor/bittensor/pull/2654 +* Fix e2e tests by @zyzniewski-reef in https://github.com/opentensor/bittensor/pull/2639 +* feat/roman/deps by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2655 +* Metagraph Improvements by @thewhaleking in https://github.com/opentensor/bittensor/pull/2659 +* add name and symbol fields to metagraph by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2658 +* Using one determine_chain_endpoint_and_network by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2661 +* Tests: separate `templates` fixture by @zyzniewski-reef in https://github.com/opentensor/bittensor/pull/2660 +* Merge `async-pre-merge-new-async` to `staging` by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2656 +* Add `set_subnet_identity_extrinsic` and related stuff by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2662 +* Changes the logging level for unexpected header keys to trace by @thewhaleking in https://github.com/opentensor/bittensor/pull/2666 +* Remove logs by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2667 +* Tests: properly handle subprocesses (subtensor, miner, validator) by @zyzniewski-reef in https://github.com/opentensor/bittensor/pull/2664 +* Last-minute requests by @thewhaleking in https://github.com/opentensor/bittensor/pull/2665 +* Updates tao_weights for mainnet by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2669 +* Update deps and default network/endpoint by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2670 + +## New Contributors +* @leopardracer made their first contribution in https://github.com/opentensor/bittensor/pull/2580 +* @Dimitrolito made their first contribution in https://github.com/opentensor/bittensor/pull/2586 +* @Marcofann made their first contribution in https://github.com/opentensor/bittensor/pull/2620 +* @HudsonGraeme made their first contribution in https://github.com/opentensor/bittensor/pull/2621 +* @crStiv made their first contribution in https://github.com/opentensor/bittensor/pull/2587 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v8.5.2...v9.0.0 + +## 9.0.0rc6 /2025-02-11 +* Using one determine_chain_endpoint_and_network by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2661 +* Tests: separate templates fixture by @zyzniewski-reef in https://github.com/opentensor/bittensor/pull/2660 +* add name and symbol fields to metagraph by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2658 +* Metagraph Improvements by @thewhaleking in https://github.com/opentensor/bittensor/pull/2659 +* feat/roman/add-subnet-identity-with-subnet-creation by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2662 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v9.0.0rc5...v9.0.0rc6 + +## 9.0.0rc5 /2025-02-07 +* Fix InfoBase + dataclasses @roman-opentensor in https://github.com/opentensor/bittensor/pull/2649 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v9.0.0rc4...v9.0.0rc5 + +## 9.0.0rc4 /2025-02-07 +* Fix for extra fields from chain data by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2647 +* Adds get_all_commitments and fixes commitment tests and query_map @thewhaleking in https://github.com/opentensor/bittensor/pull/2644 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v9.0.0rc2...v9.0.0rc3 + +## 9.0.0rc3 /2025-02-06 + +## What's Changed +* Adds methods to better accommodate the new websocket implementation (long-lived) by @thewhaleking in https://github.com/opentensor/bittensor/commit/3c44be177edef8a799c2c9dc5e49916723cab5c2 +* Adds latent-lite network by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2641 +* Updates async-substrate-interface to 1.0.0rc12 by @ibraheem-opentensor in https://github.com/opentensor/bittensor/commit/9d0b008e6163c84ed9267423324f30c3ec8af289 +* Bringing meta fields to a common form with float values float(TAO) instead of Balance and Tensor by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2642 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v9.0.0rc2...v9.0.0rc3 + +## 9.0.0rc2 /2025-02-05 + +## What's Changed +* Small bug fixes and improvements by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2637 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v9.0.0rc1...v9.0.0rc2 + +## 9.0.0rc1 /2025-02-05 + +## What's Changed +* Uses revamped Async Substrate Interface +* Compatibility with Rao changes added +* Completely revamped Async Subtensor introduced +* Numerous improvements, bug fixes, and deprecations + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v8.5.1...v9.0.0rc1 + +## 8.5.2 /2025-01-17 + +## What's Changed +* Feat/use tx pool for set weights by @camfairchild in https://github.com/opentensor/bittensor/pull/2534 +* fix get_delegates result decoding by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2551 +* [SDK] Handle server connection limit by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2553 +* Backmerge master to staging post 851 by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2557 +* [SDK] Improve InvalidStatus handler by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2558 +* [SDK] Add async version of commit reveal v3 by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2560 +* Use apt-get instead of apt for scripts by @camfairchild in https://github.com/opentensor/bittensor/pull/2571 +* fix _do_stake incorrect arguments error in staking.py by @Assh-codes in https://github.com/opentensor/bittensor/pull/2574 +* Updates tests for btwallet 3.0.0 by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2540 +* Bumps cr3 FFI by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2583 + +## New Contributors +* @Assh-codes made their first contribution in https://github.com/opentensor/bittensor/pull/2574 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v8.5.1...v8.5.2 + +## 8.5.1 /2024-12-16 + +## What's Changed +* 8.5.0 bugfixes by @thewhaleking in https://github.com/opentensor/bittensor/pull/2541 +* Removes substrate call in format_error_message by @thewhaleking in https://github.com/opentensor/bittensor/pull/2542 +* Remove torch from the weights calls by @thewhaleking in https://github.com/opentensor/bittensor/pull/2543 +* optional arg fix by @thewhaleking in https://github.com/opentensor/bittensor/pull/2544 +* async cr3 not implemented by @thewhaleking in https://github.com/opentensor/bittensor/pull/2545 +* Backmerge master to staging 851 by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2546 +* Adds retry in CRv3 by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2547 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v8.5.0...v8.5.1 + +## 8.5.0 /2024-12-12 + +## What's Changed +* add improved reveal-round params by @JohnReedV in https://github.com/opentensor/bittensor/pull/2509 +* fix: add default value to the get_block_number method in AsyncSubstrateInterface by @FLiotta in https://github.com/opentensor/bittensor/pull/2529 +* Mismatched "archive" index by @thewhaleking in https://github.com/opentensor/bittensor/pull/2530 +* Adds a factory function to create an initialised AsyncSubtensor object. by @thewhaleking in https://github.com/opentensor/bittensor/pull/2516 +* chore: fix some comments by @lvyaoting in https://github.com/opentensor/bittensor/pull/2515 +* Fixes E2E test chain buffer issues on devnet by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2531 +* Added e2e test for CRv3 + enhancements by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2532 +* Backmerge master to staging 850 by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2535 +* Enhancement/adds total stake functions by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2537 +* Fixes get_current_block by @thewhaleking in https://github.com/opentensor/bittensor/pull/2536 +* [SDK] Add `commit reveal v3` logic (python part only) by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2484 + +## New Contributors +* @JohnReedV made their first contribution in https://github.com/opentensor/bittensor/pull/2509 +* @FLiotta made their first contribution in https://github.com/opentensor/bittensor/pull/2529 +* @lvyaoting made their first contribution in https://github.com/opentensor/bittensor/pull/2515 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v8.4.5...v8.5.0 + +## 8.4.5 /2024-12-05 + +## What's Changed +* Overrides copy and deep copy for the metagraph by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2523 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v8.4.4...v8.4.5 + +## 8.4.4 /2024-12-05 + +## What's Changed +* Removes the call that automatically sets everything to warning level debugging by @thewhaleking in https://github.com/opentensor/bittensor/pull/2508 +* Exit `set_weights` on success by @thewhaleking in https://github.com/opentensor/bittensor/pull/2511 +* `test_dendrite` test clean up by @thewhaleking in https://github.com/opentensor/bittensor/pull/2512 +* Adds --logging.info level so it can be set @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2510 +* Allows wallet to be created through configs in the axon if it is provided by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2510 +* Changes verbosity level of ClientConnectorError and TimeoutError in the dendrite to debug by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2510 +* Fixes behaviour of config initialization for axon, subtensor, logging and threadpool by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2510 +* metagraph sync fix by @thewhaleking in https://github.com/opentensor/bittensor/pull/2514 +* Remove metadata retrieval of custom errors from format_error_message by @thewhaleking in https://github.com/opentensor/bittensor/pull/2518 +* Backmerge master to staging for 8.4.4 by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2519 +* Updates bt-decode to 0.4.0 by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2520 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v8.4.3...v8.4.4 + +## 8.4.3 /2024-12-02 + +## What's Changed + +* Fix logging config parsing by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2500 +* Improve `submit_extrinsic` util by @thewhaleking in https://github.com/opentensor/bittensor/pull/2502 +* Backmerge master to staging for 843 by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2505 +* WS ensure_connected socket catch by @thewhaleking in https://github.com/opentensor/bittensor/pull/2507 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v8.4.2...v8.4.3 + +## 8.4.2 /2024-11-28 + +## What's Changed + +* Fix submit_extrinsic timeout by @thewhaleking in https://github.com/opentensor/bittensor/pull/2497 +* Backmerge master to staging for 841 by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2498 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v8.4.1...v8.4.2 + +## 8.4.1 /2024-11-27 + +## What's Changed + +* Backmerge master 840 by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2494 +* Enable arguments to be set in axon config by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2493 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v8.4.0...v8.4.1 + +## 8.4.0 /2024-11-27 + +## What's Changed + +* Async unittests for `bittensor/core/extrinsics/async_weights.py` by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2425 +* Async unittests for `bittensor/core/extrinsics/async_transfer.py` by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2426 +* Async `unittests for bittensor/core/extrinsics/async_root.py` by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2427 +* Removes Conda Info by @thewhaleking in https://github.com/opentensor/bittensor/pull/2437 +* Fix typos by @omahs in https://github.com/opentensor/bittensor/pull/2440 +* [SDK] Registration related content refactoring by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2439 +* Async unittests for `bittensor/core/extrinsics/async_registration.py` by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2445 +* BittensorConsole class by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2446 +* Improve reconnection logic by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2442 +* E2E tests - Increasing Subtensor coverage (Pt 1) by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2443 +* Add python3.12 support by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2450 +* add neuron certificate discovery by @andreea-popescu-reef in https://github.com/opentensor/bittensor/pull/2267 +* Use websockets for Subtensor by @thewhaleking in https://github.com/opentensor/bittensor/pull/2455 +* Part 2: E2E tests - Increasing Subtensor coverage by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2457 +* Tests for subtensor methods related with `stake` and `unstake` extrinsics by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2458 +* Apply BittensorConsole + logging refactoring by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2452 +* Add staking and unstaking extrinsics by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2456 +* Don't strip ANSI from stdout (fixes #2365) by @vaqxai in https://github.com/opentensor/bittensor/pull/2366 +* Support fastblocks when setting root set weights in e2e tests by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2464 +* Extrinsic Submission Timeout by @thewhaleking in https://github.com/opentensor/bittensor/pull/2448 +* Resync async substrate by @thewhaleking in https://github.com/opentensor/bittensor/pull/2463 +* Fixes logging when setting weights by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2465 +* Integration tests by @thewhaleking in https://github.com/opentensor/bittensor/pull/2433 +* Fixes logic for checking block_since_last_update by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2473 +* Update unit tests websocket by @thewhaleking in https://github.com/opentensor/bittensor/pull/2468 +* Improve MockSubtensor by @thewhaleking in https://github.com/opentensor/bittensor/pull/2469 +* Fixes logging when passing multiple objects by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2477 +* Add script for solving ssl issue by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2474 +* Improve async docstrings by @thewhaleking in https://github.com/opentensor/bittensor/pull/2478 +* fix: increase stacklevel in LoggingMachine log calls by @zyzniewski-reef in https://github.com/opentensor/bittensor/pull/2476 +* remove uses of return scale obj by @thewhaleking in https://github.com/opentensor/bittensor/pull/2479 +* Backmerge master to staging for 8.4.0 by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2482 +* Expand `reuse_block` by @thewhaleking in https://github.com/opentensor/bittensor/pull/2481 +* Add NeuronInfo list from vec u8 by @camfairchild in https://github.com/opentensor/bittensor/pull/2480 +* Update `ensure_connected` for websockets by @thewhaleking in https://github.com/opentensor/bittensor/pull/2486 +* MockSubtensor work offline by @thewhaleking in https://github.com/opentensor/bittensor/pull/2487 +* Add `wait_for_block` method by @thewhaleking in https://github.com/opentensor/bittensor/pull/2489 +* Updates btwallet to 2.1.2 by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2490 +* Bumps bittensor wallet to 2.1.3 by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2492 + +## New Contributors +* @vaqxai made their first contribution in https://github.com/opentensor/bittensor/pull/2366 +* @zyzniewski-reef made their first contribution in https://github.com/opentensor/bittensor/pull/2476 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v8.3.1...v8.4.0 + +## 8.3.1 /2024-11-14 + +## What's Changed +* Fixes broken Subtensor methods by @thewhaleking in https://github.com/opentensor/bittensor/pull/2420 +* [Tests] AsyncSubtensor (Part 7: The final race) by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2418 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v8.3.0...v8.3.1 + +## 8.3.0 /2024-11-13 + +## What's Changed +* Expands the type registry to include all the available options by @thewhaleking in https://github.com/opentensor/bittensor/pull/2353 +* add `Subtensor.register`, `Subtensor.difficulty` and related staff with tests by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2352 +* added to Subtensor: `burned_register`, `get_subnet_burn_cost`, `recycle` and related extrinsics by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2359 +* Poem "Risen from the Past". Act 3. by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2363 +* default port from 9946 to 9944 by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2376 +* remove unused prometheus extrinsic by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2378 +* Replace rich.console to btlogging.loggin by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2377 +* SDK (AsyncSubtensor) Part 1 by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2374 +* SDK (AsyncSubtensor) Part 2 by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2380 +* Handle SSL Error on Connection by @thewhaleking in https://github.com/opentensor/bittensor/pull/2384 +* Avoid using `prompt` in SDK by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2382 +* Backmerge/8.2.0 by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2389 +* Remove `retry` and fix tests by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2392 +* fix: logging weights correctly in utils/weight_utils.py by @grantdfoster in https://github.com/opentensor/bittensor/pull/2362 +* Add `subvortex` subnet and tests by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2395 +* Release/8.2.1 by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2397 +* [Tests] AsyncSubtensor (Part 1) by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2398 +* Extend period for fastblock e2e tests_incentive.py by @opendansor in https://github.com/opentensor/bittensor/pull/2400 +* Remove unused import by @thewhaleking in https://github.com/opentensor/bittensor/pull/2401 +* `Reconnection substrate...` as debug by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2403 +* Handles websockets v14+ in async by @thewhaleking in https://github.com/opentensor/bittensor/pull/2404 +* [Tests] AsyncSubtensor (Part 2) by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2407 +* [Tests] AsyncSubtensor (Part 3) by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2409 +* Handle new PasswordError from btwallet by @thewhaleking in https://github.com/opentensor/bittensor/pull/2406 +* [Tests] AsyncSubtensor (Part 4) by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2410 +* [Tests] AsyncSubtensor (Part 5) by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2411 +* Bringing back lost methods for setting weights by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2412 +* Update bt-decode requirement by @thewhaleking in https://github.com/opentensor/bittensor/pull/2413 +* [Tests] AsyncSubtensor (Part 6) by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2414 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v8.2.1...v8.3.0 + +## 8.2.1 /2024-11-06 + +## What's Changed + +* Expands the type registry to include all the available options by @thewhaleking in https://github.com/opentensor/bittensor/pull/2353 +* add `Subtensor.register`, `Subtensor.difficulty` and related staff with tests by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2352 +* added to Subtensor: `burned_register`, `get_subnet_burn_cost`, `recycle` and related extrinsics by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2359 +* Poem "Risen from the Past". Act 3. by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2363 +* default port from 9946 to 9944 by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2376 +* remove unused prometheus extrinsic by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2378 +* Replace rich.console to btlogging.loggin by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2377 +* Backmerge 8.2.0 by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2389 +* Add subvortex subnet and tests by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2395 +* Handle SSL Error on Connection by @thewhaleking in https://github.com/opentensor/bittensor/pull/2384 +* Avoid using prompt in SDK by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2382 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v8.2.0...v8.2.1 + +## 8.2.0 /2024-10-10 + +## What's Changed +* remove commit from e2e tests by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2340 +* add bittensor-cli as prod deps for sdk by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2345 +* Fix the install command syntax by @rajkaramchedu in https://github.com/opentensor/bittensor/pull/2346 +* add config test by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2347 +* Bumps version for 8.2.0 by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2348 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v8.1.1...v8.2.0 + +## 8.1.1 /2024-10-04 + +## What's Changed +* Release/8.1.0 by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2332 +* Backmerge/8.1.0 by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2341 +* Bumps version and wallet by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2342 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v8.1.0...v8.1.1 + +## 8.1.0 /2024-10-03 + +## What's Changed +* Implements new logging level 'warning' by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2323 +* Adds ConnectionRefusedError in-case of connection error by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2326 +* Subtensor verbose False by default, debug logging for subtensor connected by @thewhaleking in https://github.com/opentensor/bittensor/pull/2335 +* Fix tests to be ready for rust-based bittensor-wallet by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2336 + +## 8.0.0 /2024-09-25 + +## What's Changed + +Removes Bittensor CLI and Wallet functionalities and changes the Bittensor SDK package to be light while maintaining backwards compatibility + +* Update README.md by @rajkaramchedu in https://github.com/opentensor/bittensor/pull/2320 +* remove unused code (tensor.py-> class tensor), remove old tests, add new tests by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2311 +* Updating/improving/creating docstring codebase by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2310 +* README updates for SDK by @rajkaramchedu in https://github.com/opentensor/bittensor/pull/2309 +* Improved logic for concatenating message, prefix, and suffix in bittensor logging + test by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2306 +* BTSDK: Implementation of substrait custom errors handler for bittensor by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2305 +* btsdk cleanup by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2303 +* Fix mypy error for btlogging by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2299 +* Integrate `bt_decode` into BTSDK by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2298 +* BTSDK: Corrected arguments order in logging methods + test by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2292 +* BTSDK: removed exit sys call for ConnectionRefusedError in _get_substrate by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2288 +* BTSDK: Move `do*` methods to related extrinsic by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2286 +* add reconnection logic for correctly closed connection by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2283 +* Move extrinsics, update `deprecated.py` module. by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2278 +* Add substrate reconnection logic by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2269 +* Prod requirements cleanup by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2266 +* Decoupling chain_data.py to sub-package by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2264 +* Increase Bittensor SDK test coverage by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2262 +* Increase SDK test coverage (Part3) by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2257 +* Increase bittensor SDK test coverage by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2256 +* Increase test coverage for subtensor.py by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2252 +* Adds e2e and fixes metagraph save()/load() by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2231 +* feat/roman/reafctoring-before-test-coverage by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2230 +* Enhance: Switch from format() to f-strings by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2228 +* Commit-reveal re-added & e2e coverage by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2224 +* Adds e2e setup & tests by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2221 +* Updates after review session by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2220 +* Fix the usage of env vars in default settings. by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2218 +* Add dendrite reference to backwords compatibility by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2217 +* Bringing `btsdk` up-to-date with `staging` branch. by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2210 +* Part 3: Create new 'bittensor-sdk` package by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2206 +* Part 2: Redesign, fix namespace conflicts, remove btcli by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2204 +* Part1: Removing content related to the wallet. Start use the pip installable package. by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2191 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v7.4.0...v8.0.0 + +## 7.4.0 /2024-08-29 + +## What's Changed +* [Fix] Allow unstake below network min by @camfairchild in https://github.com/opentensor/bittensor/pull/2016 +* Tests/e2e tests staging by @open-junius in https://github.com/opentensor/bittensor/pull/1943 +* Chore: Backmerge 7.2 by @gus-opentensor in https://github.com/opentensor/bittensor/pull/2020 +* Fix broken tests and Enforce BTCLI usage by @opendansor in https://github.com/opentensor/bittensor/pull/2027 +* Add time delay to faucet by @opendansor in https://github.com/opentensor/bittensor/pull/2030 +* Skip faucet test by @opendansor in https://github.com/opentensor/bittensor/pull/2031 +* Adds normalization for alpha hyperparams by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2035 +* Revert info logging in processing response by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2043 +* Pin numpy version to 1.26.4 in prod.txt by @rajkaramchedu in https://github.com/opentensor/bittensor/pull/2045 +* Test hot key Swap by @opendansor in https://github.com/opentensor/bittensor/pull/2044 +* Do not run Circle-CI on drafts by @thewhaleking in https://github.com/opentensor/bittensor/pull/1959 +* Enhancement: Detailed nonce information in-case of failures by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2050 +* fix bittensor not installing under Python 3.13 by @mjurbanski-reef in https://github.com/opentensor/bittensor/pull/2053 +* Enable Faucet Test by @opendansor in https://github.com/opentensor/bittensor/pull/2056 +* Add back BT_SUBTENSOR_CHAIN_ENDPOINT env variable by @bradleytf in https://github.com/opentensor/bittensor/pull/2034 +* Fix: Logging configs not being set by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2065 +* Feature/gus/liquid alpha params by @gus-opentensor in https://github.com/opentensor/bittensor/pull/2012 +* Test Emissions E2E by @opendansor in https://github.com/opentensor/bittensor/pull/2036 +* Prevent e2e draft by @opendansor in https://github.com/opentensor/bittensor/pull/2072 +* Fix e2e to only run when PR is ready for review by @opendansor in https://github.com/opentensor/bittensor/pull/2077 +* Fix Faucet and fastblocks interaction by @opendansor in https://github.com/opentensor/bittensor/pull/2083 +* Float normalization for child hotkeys by @opendansor in https://github.com/opentensor/bittensor/pull/2093 +* Fix e2e test hanging by @open-junius in https://github.com/opentensor/bittensor/pull/2118 +* Fixes leaked semaphores by @thewhaleking in https://github.com/opentensor/bittensor/pull/2125 +* Backmerge master -> staging by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2136 +* fix: coldkeypub usage instead of coldkey for arbitration_stats by @Rapiiidooo in https://github.com/opentensor/bittensor/pull/2132 +* Removes extra no_prompts in commands by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2140 +* Adds timeout for e2e tests by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2141 +* fix: updates test_axon verify body async tests by @gus-opentensor in https://github.com/opentensor/bittensor/pull/2142 +* test: fix mocksubtensor query previous blocks by @timabilov in https://github.com/opentensor/bittensor/pull/2139 +* Adds E2E for Metagraph command by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2143 +* feat: Enhance dendrite error messaging by @gus-opentensor in https://github.com/opentensor/bittensor/pull/2117 +* Adds E2E Tests for wallet creation commands by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2145 +* [Ledger Integration] [Feature] bump pysub to 1.7.9+ by @camfairchild in https://github.com/opentensor/bittensor/pull/2156 +* Ruff complains about an extra line by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2158 +* support Wallet names with hyphens when passing password through ENV vars by @mjurbanski-reef in https://github.com/opentensor/bittensor/pull/1949 +* Fix naming convention of swap hotkey test by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2162 +* Adds E2E test for wallet regenerations + fixes input bug for regen hotkey by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2149 +* Backmerge Master -> Staging (7.4) by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2170 +* ci: auto assigns cortex to opened PRs by @gus-opentensor in https://github.com/opentensor/bittensor/pull/2184 +* CI/E2E test improvements by @mvds00 in https://github.com/opentensor/bittensor/pull/2168 +* Fix multiprocessing POW errors and No Torch logging errors by @thewhaleking in https://github.com/opentensor/bittensor/pull/2186 +* ci: update reviewers by @gus-opentensor in https://github.com/opentensor/bittensor/pull/2189 +* Adds updated type in timeouts dendrite by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2196 +* Bumps setuptools ~=70.0.0 by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2150 +* Bump black from 23.7.0 to 24.3.0 in /requirements by @dependabot in https://github.com/opentensor/bittensor/pull/2197 +* btlogging/loggingmachine.py: Fix bw compat API. by @mvds00 in https://github.com/opentensor/bittensor/pull/2155 +* Check for participation before nomination call by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2193 +* test: subnet list e2e by @gus-opentensor in https://github.com/opentensor/bittensor/pull/2198 +* ensure msg is str in _concat_msg by @thewhaleking in https://github.com/opentensor/bittensor/pull/2200 +* Fixes tests depending on explicit line numbers by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2211 +* Merge streaming fix to staging by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2183 +* Multiple bittensor versions e2e workflow by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2212 +* Changes name of workflow file by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2213 +* Enhances e2e tests to contain assertions & logging by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2192 +* Security fix: Bumps ansible and certifi by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2214 +* Wallet List Command e2e test by @gus-opentensor in https://github.com/opentensor/bittensor/pull/2207 +* fix Synapse base performance (more than 10x speed up) by @mjurbanski-reef in https://github.com/opentensor/bittensor/pull/2161 +* Child Hotkeys by @opendansor in https://github.com/opentensor/bittensor/pull/2071 +* Improve child hotkeys QOL by @opendansor in https://github.com/opentensor/bittensor/pull/2225 +* Child hotkeys handle excess normalization by @opendansor in https://github.com/opentensor/bittensor/pull/2229 +* Fixes chain compilation timeouts by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2238 +* Update Child Hotkey commands by @opendansor in https://github.com/opentensor/bittensor/pull/2245 +* feat: return error message instead of raising exception by @gus-opentensor in https://github.com/opentensor/bittensor/pull/2244 +* Backmerge master to staging (7.3.1) by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2254 + +## New Contributors +* @bradleytf made their first contribution in https://github.com/opentensor/bittensor/pull/2034 +* @Rapiiidooo made their first contribution in https://github.com/opentensor/bittensor/pull/2132 +* @timabilov made their first contribution in https://github.com/opentensor/bittensor/pull/2139 +* @mvds00 made their first contribution in https://github.com/opentensor/bittensor/pull/2168 +* @dependabot made their first contribution in https://github.com/opentensor/bittensor/pull/2197 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v7.3.1...v7.4.0 + +## 7.3.1 / 2024-08-19 + +## What's Changed +* https://github.com/opentensor/bittensor/pull/2156 by @camfairchild + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v7.3.0...v7.3.1 + +## 7.3.0 / 2024-07-12 + +## What's Changed +* Liquid Alpha by @opendansor & @gus-opentensor in https://github.com/opentensor/bittensor/pull/2012 +* check_coldkey_swap by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2126 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v7.2.0...v7.3.0 + + +## 7.2.0 / 2024-06-12 + +## What's Changed +* less verbose handled synapse exceptions by @mjurbanski-reef in https://github.com/opentensor/bittensor/pull/1928 +* Clean up the imports in commands/stake.py by @thewhaleking in https://github.com/opentensor/bittensor/pull/1951 +* Fix E2E test for Commit/Reveal with Salt flag by @opendansor in https://github.com/opentensor/bittensor/pull/1952 +* `bittensor.chain_data.py` module refactoring. by @RomanCh-OT in https://github.com/opentensor/bittensor/pull/1955 +* ci: e2e tests by @orriin in https://github.com/opentensor/bittensor/pull/1915 +* Dependency cleanup by @mjurbanski-reef in https://github.com/opentensor/bittensor/pull/1967 +* replace `black` with `ruff` by @mjurbanski-reef in https://github.com/opentensor/bittensor/pull/1968 +* post-black to ruff migration cleanup by @mjurbanski-reef in https://github.com/opentensor/bittensor/pull/1979 +* Revert Axon IP decoding changes by @camfairchild in https://github.com/opentensor/bittensor/pull/1981 +* A wrapper for presenting extrinsics errors in a human-readable form. by @RomanCh-OT in https://github.com/opentensor/bittensor/pull/1980 +* Feat: Added normalized hyperparams by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/1891 +* deprecate nest_asyncio use by @mjurbanski-reef in https://github.com/opentensor/bittensor/pull/1974 +* Add e2e test for axon by @opendansor in https://github.com/opentensor/bittensor/pull/1984 +* Dendrite E2E test by @opendansor in https://github.com/opentensor/bittensor/pull/1988 +* fix __version_as_int__ for >10 minor/patch release vers (resolves #1982) by @mjurbanski-reef in https://github.com/opentensor/bittensor/pull/1993 +* Test Incentive E2E by @opendansor in https://github.com/opentensor/bittensor/pull/2002 +* Add E2E faucet test by @opendansor in https://github.com/opentensor/bittensor/pull/1987 +* Allow unstake below network min by @camfairchild in https://github.com/opentensor/bittensor/pull/2016 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v7.1.1...v7.2.0 + + +## 7.1.1 / 2024-06-11 + +## What's Changed +* commit_reveal_weights_enabled argument parsing hotfix by @camfairchild in https://github.com/opentensor/bittensor/pull/2003 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v7.1.0...v7.1.1 + +## 7.1.0 / 2024-06-05 + +## What's Changed +* Added _do_set_root_weights by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/1838 +* Release/7.0.1 by @gus-opentensor in https://github.com/opentensor/bittensor/pull/1963 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v7.0.1...v7.1.0 + +## 7.0.1 / 2024-05-31 + +## What's Changed +* Release/7.0.0 by @gus-opentensor in https://github.com/opentensor/bittensor/pull/1899 +* Fix return of ip version. by @opendansor in https://github.com/opentensor/bittensor/pull/1961 +* Fix trigger use_torch() by @renesweet24 https://github.com/opentensor/bittensor/pull/1960 + +## New Contributors +* @renesweet24 made their first contribution in https://github.com/opentensor/bittensor/pull/1960 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v7.0.0...v7.0.1 + + +## 7.0.0 / 2024-05-29 + +## What's Changed +* replace torch with numpy by @andreea-popescu-reef in https://github.com/opentensor/bittensor/pull/1777 +* Fix broken link in contrib/RELEASE_GUIDELINES #1821 by @thewhaleking in https://github.com/opentensor/bittensor/pull/1823 +* Tests: Added coverage for set_weights by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/1825 +* Remove irrelevant call to get_delegates method. by @RomanCh-OT in https://github.com/opentensor/bittensor/pull/1826 +* Support for string mnemonic thru cli when regenerating coldkeys by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/1815 +* Logging: Added _primary_loggers by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/1797 +* Add in check for minimum stake for unstaking by @thewhaleking in https://github.com/opentensor/bittensor/pull/1832 +* Cache get_decoder_class by @thewhaleking in https://github.com/opentensor/bittensor/pull/1834 +* Warmfix/change decoder cacheing by @thewhaleking in https://github.com/opentensor/bittensor/pull/1842 +* Fix typo in warmfix by @thewhaleking in https://github.com/opentensor/bittensor/pull/1844 +* Add the command btcli root list_delegates_lite to handle the Delegate… by @RomanCh-OT in https://github.com/opentensor/bittensor/pull/1840 +* Change: console.error => console.print by @thewhaleking in https://github.com/opentensor/bittensor/pull/1849 +* Small fix with receiving delegates based on a 4-hour archive block by @RomanCh-OT in https://github.com/opentensor/bittensor/pull/1854 +* Replace torch with numpy by @sepehr-opentensor in https://github.com/opentensor/bittensor/pull/1786 +* Versioning: Enforcement for eth-utils by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/1852 +* Versioning: Dependencies for FastAPI for Apple M's by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/1855 +* Retrieving error types from the metadata of the Substrate palette SubtensorModule for the btcli console (logic) by @RomanCh-OT in https://github.com/opentensor/bittensor/pull/1862 +* Add version check caching, fix version comparison by @olzhasar-reef in https://github.com/opentensor/bittensor/pull/1835 +* Tests: Added coverage for root.py by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/1877 +* Tests: Added coverage for network.py by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/1879 +* Tests: extends coverage for overview cmd part 1 by @gus-opentensor in https://github.com/opentensor/bittensor/pull/1873 +* Tests: Added coverage for Unstaking by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/1878 +* Tests: Added coverage for staking by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/1837 +* Tests: Added coverage for Delegation by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/1874 +* Updated error message and a test typo. by @thewhaleking in https://github.com/opentensor/bittensor/pull/1871 +* fix: deprecated usage of `Balances::transfer` method by @orriin in https://github.com/opentensor/bittensor/pull/1886 +* Fix Type Annotation by @opendansor in https://github.com/opentensor/bittensor/pull/1895 +* Docstrings updates for list delegate lite feature by @rajkaramchedu in https://github.com/opentensor/bittensor/pull/1890 +* Add Pre-commit Checker in scripts. Helps reduce CI calls. by @RomanCh-OT in https://github.com/opentensor/bittensor/pull/1893 +* fix get_coldkey_password_from_environment resolving wrong password by @mjurbanski-reef in https://github.com/opentensor/bittensor/pull/1843 +* Drop python 3.8 support by @mjurbanski-reef in https://github.com/opentensor/bittensor/pull/1892 +* feat: Refactor phase 2 overview cmd & add test cov. Adds factories by @gus-opentensor in https://github.com/opentensor/bittensor/pull/1887 +* Add setting delegate take by @gztensor in https://github.com/opentensor/bittensor/pull/1903 +* E2E Test Patterns by @orriin in https://github.com/opentensor/bittensor/pull/1885 +* chore: correct method types by @distributedstatemachine in https://github.com/opentensor/bittensor/pull/1907 +* bittensor.btlogging refactoring by @RomanCh-OT in https://github.com/opentensor/bittensor/pull/1896 +* Part 1 for refactoring bittensor/subtensor.py by @RomanCh-OT in https://github.com/opentensor/bittensor/pull/1911 +* Update: Pydantic V2 by @opendansor in https://github.com/opentensor/bittensor/pull/1889 +* Add back compatibility with torch by @thewhaleking in https://github.com/opentensor/bittensor/pull/1904 +* Release/6.12.2 by @gus-opentensor in https://github.com/opentensor/bittensor/pull/1910 +* Chore: Updated dev requirements by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/1946 + +## New Contributors +* @andreea-popescu-reef made their first contribution in https://github.com/opentensor/bittensor/pull/1777 +* @thewhaleking made their first contribution in https://github.com/opentensor/bittensor/pull/1823 +* @RomanCh-OT made their first contribution in https://github.com/opentensor/bittensor/pull/1826 +* @olzhasar-reef made their first contribution in https://github.com/opentensor/bittensor/pull/1835 +* @orriin made their first contribution in https://github.com/opentensor/bittensor/pull/1886 +* @opendansor made their first contribution in https://github.com/opentensor/bittensor/pull/1895 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v6.12.2...v7.0.0 + +## 6.12.2 / 2024-05-20 + +## What's Changed +* Add setting delegate take +* fix: deprecated transfer method usage + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v6.12.1...54eee604c00ac4f04a31d5d7bc663124731a34d8 + + +## 6.12.1 / 2024-05-17 + +## What's Changed +* Hotfix if the subnet UID is not in the Subnets + + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v6.12.0...fd2442db8bb8aad55ced2ac3b748b04ebdc73292 + + + +## 6.12.0 / 2024-04-29 + +## What's Changed +* Tests: Axon to_string patch import by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/1785 +* Tests: Extends coverage on Serving extrinsics methods by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/1783 +* Fix: CVE-2024-24762 FastAPI by @gus-opentensor in https://github.com/opentensor/bittensor/pull/1800 +* Fix: CVE-2024-26130 | vulnerability cryptography by @gus-opentensor in https://github.com/opentensor/bittensor/pull/1801 +* fix PR templates by @mjurbanski-reef in https://github.com/opentensor/bittensor/pull/1778 +* Fix: SNYK-PYTHON-CERTIFI-5805047 | Vulnerability Certifi by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/1816 +* Tests: Extends test coverage on Registration methods by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/1814 +* Fix: Wallet overwrite functionality by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/1802 + + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v6.11.0...v6.12.0 + +## 6.11.0 / 2024-04-11 + +## What's Changed +* Tests: Adds coverage to subtensor help method & determine_chain_endpoint_and_network by @gus-opentensor in https://github.com/opentensor/bittensor/pull/1761 +* [bug fix] Fix import json by @camfairchild in https://github.com/opentensor/bittensor/pull/1759 +* Remove context management for substrate in subtensor by @sepehr-opentensor in https://github.com/opentensor/bittensor/pull/1766 +* Tests: Extends coverage on axon methods by @gus-opentensor in https://github.com/opentensor/bittensor/pull/1769 +* Revert nonce implementation fix by @ifrit98 in https://github.com/opentensor/bittensor/pull/1774 +* remove tests from package distribution by @mjurbanski-reef in https://github.com/opentensor/bittensor/pull/1779 +* Tests: Extends test coverage on Senate methods by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/1781 + +## New Contributors +* @mjurbanski-reef made their first contribution in https://github.com/opentensor/bittensor/pull/1779 +* @ibraheem-opentensor made their first contribution in https://github.com/opentensor/bittensor/pull/1781 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v6.10.1...v6.11.0 +## 6.10.1 / 2024-04-05 +## What's Changed +* Revert nonce implementation fix #1774: Breaking change needs to telegraphed in next release. + +## 6.10.0 / 2024-03-25 + +## What's Changed +* handle req args by parsing and raising by @ifrit98 in https://github.com/opentensor/bittensor/pull/1733 +* Replace wildcard imports with specific imports by @brueningf in https://github.com/opentensor/bittensor/pull/1724 +* Logging Refactor by @sepehr-opentensor in https://github.com/opentensor/bittensor/pull/1751 +* Update DEBUGGING.md by @e-gons in https://github.com/opentensor/bittensor/pull/1755 +* fix: nonce implementation by @GentikSolm in https://github.com/opentensor/bittensor/pull/1754 + +## New Contributors +* @sepehr-opentensor made their first contribution in https://github.com/opentensor/bittensor/pull/1751 +* @e-gons made their first contribution in https://github.com/opentensor/bittensor/pull/1755 +* @GentikSolm made their first contribution in https://github.com/opentensor/bittensor/pull/1754 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v6.9.3...v6.10.0 + +## 6.9.3 / 2024-03-12 + +## What's Changed +* Release/6.9.2 by @ifrit98 in https://github.com/opentensor/bittensor/pull/1743 + + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v6.9.2...v6.9.3 + + +## 6.9.2 / 2024-03-08 + +## What's Changed +* Change error into a warning if not using archive. Impossible to tell if local is lite or full node. + + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v6.9.1...v6.9.2 + + +## 6.9.1 / 2024-03-08 + +## What's Changed +* Hotfix for reversing comparison operator for block checking to raise error if not using archive nodes + + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v6.9.0...v6.9.1 + + +## 6.9.0 / 2024-03-07 + +## What's Changed +* Doc: Updates WalletBalanceCommand docstring by @gus-opentensor in https://github.com/opentensor/bittensor/pull/1716 +* feature: metapgraph.py now passing type check by @gus-opentensor in https://github.com/opentensor/bittensor/pull/1721 +* fix: Updates `btcli wallet balance --all` to get proper Wallet Name & Coldkey Address sets by @gus-opentensor in https://github.com/opentensor/bittensor/pull/1720 +* Feature/prompt set identity on btcli/phil by @ifrit98 in https://github.com/opentensor/bittensor/pull/1719 +* Fix: Raises error when exceeding block max on metagraph by @gus-opentensor in https://github.com/opentensor/bittensor/pull/1722 +* Release/6.8.2 by @ifrit98 in https://github.com/opentensor/bittensor/pull/1730 +* Expands type checking to subtensor by @gus-opentensor in https://github.com/opentensor/bittensor/pull/1731 +* Feature: Synapse passing type check by @gus-opentensor in https://github.com/opentensor/bittensor/pull/1725 +* bump req for security vulnerability in crpytography by @ifrit98 in https://github.com/opentensor/bittensor/pull/1718 +* Fix: proper association with wallet dir and coldkey addr #1739 by @gus-opentensor & @sepehr-opentensor +* Fixed event lookup on new network added #1741 by @shibshib + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v6.8.2...v6.9.0 + + +## 6.8.2 / 2024-03-01 + +## What's Changed +* Set weights fix retry and check mechanism by @ifrit98 in https://github.com/opentensor/bittensor/pull/1729 + + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v6.8.1...v6.8.2 + + +## 6.8.1 / 2024-02-22 + +## What's Changed +* Hotfix revert dendrite streaming call to use `synapse.process_streaming_response` func instead of Starlette `iter_any()` from response object. + + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v6.8.0...v6.8.1 + + +## 6.8.0 / 2024-02-16 + +## What's Changed +* Release/6.7.2 by @ifrit98 in https://github.com/opentensor/bittensor/pull/1695 +* close synchronosuly on __del__ by @ifrit98 in https://github.com/opentensor/bittensor/pull/1700 +* CI: Flake8 by @gus-opentensor in https://github.com/opentensor/bittensor/pull/1701 +* logging off switch by @ifrit98 in https://github.com/opentensor/bittensor/pull/1704 +* Extrinsic update by @ifrit98 in https://github.com/opentensor/bittensor/pull/1703 +* Bittensor shared request layer by @ifrit98 in https://github.com/opentensor/bittensor/pull/1698 +* Add no_prompt argument to help printout in https://github.com/opentensor/bittensor/pull/1707 +* Adds mypi typechecking to circleci by @gus-opentensor in https://github.com/opentensor/bittensor/pull/1705 +* Remove set weights ttl now that we have a better extrinsic method by @ifrit98 +* Bug fix in overview command for dereg stake with outdated `stake_info` object fields by @ifrit98 in https://github.com/opentensor/bittensor/pull/1712 +* Moves mock wallet creation to temp dir by @gus-opentensor in https://github.com/opentensor/bittensor/pull/1711 + + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v6.7.2...v6.8.0 + + +## 6.7.2 / 2024-02-08 + +## What's Changed +* Release/6.7.1 by @ifrit98 in https://github.com/opentensor/bittensor/pull/1688 +* Increases test coverage for cli & chain_data by @gus-opentensor in https://github.com/opentensor/bittensor/pull/1690 +* Subtensor/update pysubstrate latest/phil by @ifrit98 in https://github.com/opentensor/bittensor/pull/1684 +* Update staging to latest master by @ifrit98 in https://github.com/opentensor/bittensor/pull/1691 +* return messages with subtensor extrinsic to set weights by @ifrit98 in https://github.com/opentensor/bittensor/pull/1692 +* Logging/debug to trace axon by @ifrit98 in https://github.com/opentensor/bittensor/pull/1694 + + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v6.7.1...v6.7.2 + + +## 6.7.1 / 2024-02-02 + +## What's Changed +* Release/6.7.0 by @ifrit98 in https://github.com/opentensor/bittensor/pull/1674 +* Eighth (final) docstrings formatting PR by @rajkaramchedu in https://github.com/opentensor/bittensor/pull/1678 +* Sixth docstrings formatting PR by @rajkaramchedu in https://github.com/opentensor/bittensor/pull/1676 +* Seventh docstrings formatting PR by @rajkaramchedu in https://github.com/opentensor/bittensor/pull/1677 +* Update README.md by @unconst in https://github.com/opentensor/bittensor/pull/1679 +* Update README.md by @unconst in https://github.com/opentensor/bittensor/pull/1680 +* black formatting by @ifrit98 in https://github.com/opentensor/bittensor/pull/1685 +* burn -> recycle for public facing code by @ifrit98 in https://github.com/opentensor/bittensor/pull/1681 +* Expands test coverage and coverts python unittest classes to pure pytest by @gus-opentensor in https://github.com/opentensor/bittensor/pull/1686 +* wrap set weights in a ttl multiprocessing call so we don't hang past TTL by @ifrit98 in https://github.com/opentensor/bittensor/pull/1687 + +## New Contributors +* @gus-opentensor made their first contribution in https://github.com/opentensor/bittensor/pull/1686 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v6.7.0...v6.7.1 + + + +## 6.7.0 / 2024-01-25 + +## What's Changed +* First docstrings formatting PR by @rajkaramchedu in https://github.com/opentensor/bittensor/pull/1663 +* Second docstrings formatting PR by @rajkaramchedu in https://github.com/opentensor/bittensor/pull/1665 +* Third docstrings formatting PR by @rajkaramchedu in https://github.com/opentensor/bittensor/pull/1666 +* updated mac yaml mac yaml by @dougsillars in https://github.com/opentensor/bittensor/pull/1668 +* Fourth docstrings formatting PR by @rajkaramchedu in https://github.com/opentensor/bittensor/pull/1670 +* Fifth docstrings formatting PR by @rajkaramchedu in https://github.com/opentensor/bittensor/pull/1671 +* ensure branch off from staging and rm old docs by @ifrit98 in https://github.com/opentensor/bittensor/pull/1667 +* staging black format fix by @ifrit98 in https://github.com/opentensor/bittensor/pull/1669 +* wallet history url for taostats by @ifrit98 in https://github.com/opentensor/bittensor/pull/1672 +* take bt.config as a first argument regardless if specified by @ifrit98 in https://github.com/opentensor/bittensor/pull/1664 +* Hparams update by @ifrit98 in https://github.com/opentensor/bittensor/pull/1673 + +## New Contributors +* @rajkaramchedu made their first contribution in https://github.com/opentensor/bittensor/pull/1663 +* @dougsillars made their first contribution in https://github.com/opentensor/bittensor/pull/1668 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v6.6.1...v6.7.0 + + +## 6.6.1 / 2024-01-17 + +## What's Changed +* bittensor README update by @Jackalgirl in https://github.com/opentensor/bittensor/pull/1650 +* Bugfix btcli fix args by @ifrit98 in https://github.com/opentensor/bittensor/pull/1654 + +## New Contributors +* @Jackalgirl made their first contribution in https://github.com/opentensor/bittensor/pull/1650 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v6.6.0...v6.6.1 + + +## 6.6.0 / 2024-01-08 + +## What's Changed +* Add commitment support to MockSubtensor by @agoncharov-reef in https://github.com/opentensor/bittensor/pull/1635 +* don't prenormalize weights in btcli boost/slash by @ifrit98 in https://github.com/opentensor/bittensor/pull/1636 +* feat(wallets.py): add wallet history command by @saqib-codes-11 in https://github.com/opentensor/bittensor/pull/1638 +* Update taostats link by @mogmachine in https://github.com/opentensor/bittensor/pull/1641 +* update wallet history command to right justify and fmt 3 decimal places by @ifrit98 in https://github.com/opentensor/bittensor/pull/1639 + +## New Contributors +* @agoncharov-reef made their first contribution in https://github.com/opentensor/bittensor/pull/1635 +* @saqib-codes-11 made their first contribution in https://github.com/opentensor/bittensor/pull/1638 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v6.5.0...v6.6.0 + + +## 6.5.0 / 2023-12-19 + +## What's Changed +* Logging/axon handling refactor by @ifrit98 in https://github.com/opentensor/bittensor/pull/1627 +* Add decoding to get_commitment helper function to return original value by @ifrit98 in https://github.com/opentensor/bittensor/pull/1630 +* don't print subtensor message on cli by @ifrit98 in https://github.com/opentensor/bittensor/pull/1625 +* Add tab autocompletion to btcli by @ifrit98 in https://github.com/opentensor/bittensor/pull/1628 + + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v6.4.4...v6.5.0 + + +## 6.4.4 / 2023-12-14 + +## What's Changed + +* Merge/master642 staging no-ff by @ifrit98 in https://github.com/opentensor/bittensor/pull/1615 +* print help message on error for subcommands by @ifrit98 in https://github.com/opentensor/bittensor/pull/1618 +* Metadata/commitments by @ifrit98 in https://github.com/opentensor/bittensor/pull/1621 + +## New Contributors +* @omahs made their first contribution in https://github.com/opentensor/bittensor/pull/1553 +* @surcyf123 made their first contribution in https://github.com/opentensor/bittensor/pull/1569 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v6.4.2...v6.4.4 + + +## 6.4.2 / 2023-12-07 + +## What's Changed +* Fix broken explorer links https://github.com/opentensor/bittensor/pull/1607 +* Fix spamming bittensor subtensor logging https://github.com/opentensor/bittensor/pull/1608 +* Fix hanging subtensor websocket https://github.com/opentensor/bittensor/pull/1609 +* Hparam update to palette: https://github.com/opentensor/bittensor/pull/1612 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v6.4.1...v6.4.2 + + +## 6.4.1 / 2023-12-01 + +## What's Changed +* add helpful messages to signal coming changes in https://github.com/opentensor/bittensor/pull/1600/commits/86c0c3ccfcd91d0e3ff87f53bdc3e9c5e68661da +* revert default subtensor network to finney in https://github.com/opentensor/bittensor/pull/1600/commits/8c69a3c15cd556384d0309e951f0a9b164dd36cb + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v6.4.0...v6.4.1 + + +## 6.4.0 / 2023-11-29 + +## What's Changed +* (un)Staking multiple avoid tx limit by @camfairchild in https://github.com/opentensor/bittensor/pull/1244 +* additional logging for prometheus by @Eugene-hu in https://github.com/opentensor/bittensor/pull/1246 +* Dataset fix by @isabella618033 in https://github.com/opentensor/bittensor/pull/1249 +* Grab delegates details from GitHub by @camfairchild in https://github.com/opentensor/bittensor/pull/1245 +* Add raw spec for local test and new bins by @camfairchild in https://github.com/opentensor/bittensor/pull/1243 +* Fix list_delegates on non-archive nodes by @camfairchild in https://github.com/opentensor/bittensor/pull/1232 +* Blacklist fixes + depreciation of old signatures by @Eugene-hu in https://github.com/opentensor/bittensor/pull/1240 +* [BIT-636] Change u16 weight normalization to max-upscaling by @opentaco in https://github.com/opentensor/bittensor/pull/1241 +* remove duplicate command #1228 by @camfairchild in https://github.com/opentensor/bittensor/pull/1231 +* test_forward_priority_2nd_request_timeout fix by @isabella618033 in https://github.com/opentensor/bittensor/pull/1276 +* Remove btcli query and btcli set_weights by @camfairchild in https://github.com/opentensor/bittensor/pull/1144 +* Merge releases 4.0.0 and 4.0.1 back to staging by @camfairchild in https://github.com/opentensor/bittensor/pull/1306 +* Improve development workflow documentation by @quac88 in https://github.com/opentensor/bittensor/pull/1262 +* staging updates and fixes by @ifrit98 in https://github.com/opentensor/bittensor/pull/1540 +* Add root get_weights command to btcli by @Rubberbandits in https://github.com/opentensor/bittensor/pull/1536 +* Fix typo by @steffencruz in https://github.com/opentensor/bittensor/pull/1543 +* remove duplicated debug message in dendrite by @ifrit98 in https://github.com/opentensor/bittensor/pull/1544 +* Cli fix by @ifrit98 in https://github.com/opentensor/bittensor/pull/1541 +* update faucet helpstr by @ifrit98 in https://github.com/opentensor/bittensor/pull/1542 +* Added mechanism to sum all delegated tao by @shibshib in https://github.com/opentensor/bittensor/pull/1547 +* Dict hash fix by @ifrit98 in https://github.com/opentensor/bittensor/pull/1548 +* Release/6.1.0 by @ifrit98 in https://github.com/opentensor/bittensor/pull/1550 +* Merge master by @ifrit98 in https://github.com/opentensor/bittensor/pull/1552 +* Streaming fix by @ifrit98 in https://github.com/opentensor/bittensor/pull/1551 +* Fix typos by @omahs in https://github.com/opentensor/bittensor/pull/1553 +* Normalize weights in r get weights table by @camfairchild in https://github.com/opentensor/bittensor/pull/1556 +* Dendrite & Synapse updates and fixes by @ifrit98 in https://github.com/opentensor/bittensor/pull/1555 +* rm root flag in metagraph by @ifrit98 in https://github.com/opentensor/bittensor/pull/1558 +* Max Faucet Runs == 3 by @ifrit98 in https://github.com/opentensor/bittensor/pull/1560 +* replace unknown wallet params (chain mismatch) with key values by @ifrit98 in https://github.com/opentensor/bittensor/pull/1559 +* Remove PoW registration cli and associated extrinsic by @ifrit98 in https://github.com/opentensor/bittensor/pull/1557 +* Add btcli wallet balance by @ifrit98 in https://github.com/opentensor/bittensor/pull/1564 +* Dendrite fixes by @ifrit98 in https://github.com/opentensor/bittensor/pull/1561 +* Master into staging by @ifrit98 in https://github.com/opentensor/bittensor/pull/1570 +* adding logging.exception by @surcyf123 in https://github.com/opentensor/bittensor/pull/1569 +* Update network.py by @wildcommunist in https://github.com/opentensor/bittensor/pull/1568 +* Subtensor Registry by @Eugene-hu in https://github.com/opentensor/bittensor/pull/1562 +* add instructions for upgrading bittensor with outdated version check by @ifrit98 in https://github.com/opentensor/bittensor/pull/1571 +* Add identity commands to btcli by @ifrit98 in https://github.com/opentensor/bittensor/pull/1566 +* Add set_delegate_take command to btcli by @Rubberbandits in https://github.com/opentensor/bittensor/pull/1563 +* Subtensor archive by @ifrit98 in https://github.com/opentensor/bittensor/pull/1575 +* Bugfix/list delegates by @ifrit98 in https://github.com/opentensor/bittensor/pull/1577 +* don't return result twice in query() by @ifrit98 in https://github.com/opentensor/bittensor/pull/1574 +* rename logging.py so doesn't circ import by @ifrit98 in https://github.com/opentensor/bittensor/pull/1572 +* add AxonInfo._string() by @ifrit98 in https://github.com/opentensor/bittensor/pull/1565 +* don't print __is_set for recursive objects by @ifrit98 in https://github.com/opentensor/bittensor/pull/1573 +* Adds docstrings for CLI for Sphynx documentation by @ifrit98 in https://github.com/opentensor/bittensor/pull/1579 +* Master 630 into staging by @ifrit98 in https://github.com/opentensor/bittensor/pull/1590 +* Registry cost 0.1 tao by @Eugene-hu in https://github.com/opentensor/bittensor/pull/1587 +* Add swap_hotkey command to wallet by @ifrit98 in https://github.com/opentensor/bittensor/pull/1580 +* Cuda fix by @ifrit98 in https://github.com/opentensor/bittensor/pull/1595 +* Feature/local subtensor default by @ifrit98 in https://github.com/opentensor/bittensor/pull/1591 +* Boost by @unconst in https://github.com/opentensor/bittensor/pull/1594 +* avoid aiohttp <3.9.0 potential security issue by @ifrit98 in https://github.com/opentensor/bittensor/pull/1597 +* update bittensor docstrings (overhaul) by @ifrit98 in https://github.com/opentensor/bittensor/pull/1592 + +## New Contributors +* @omahs made their first contribution in https://github.com/opentensor/bittensor/pull/1553 +* @surcyf123 made their first contribution in https://github.com/opentensor/bittensor/pull/1569 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v6.0.1...v6.4.0 + + +## 6.3.0 / 2023-11-16 + +## What's Changed +* (un)Staking multiple avoid tx limit by @camfairchild in https://github.com/opentensor/bittensor/pull/1244 +* additional logging for prometheus by @Eugene-hu in https://github.com/opentensor/bittensor/pull/1246 +* Dataset fix by @isabella618033 in https://github.com/opentensor/bittensor/pull/1249 +* Grab delegates details from GitHub by @camfairchild in https://github.com/opentensor/bittensor/pull/1245 +* Add raw spec for local test and new bins by @camfairchild in https://github.com/opentensor/bittensor/pull/1243 +* Fix list_delegates on non-archive nodes by @camfairchild in https://github.com/opentensor/bittensor/pull/1232 +* Blacklist fixes + depreciation of old signatures by @Eugene-hu in https://github.com/opentensor/bittensor/pull/1240 +* [BIT-636] Change u16 weight normalization to max-upscaling by @opentaco in https://github.com/opentensor/bittensor/pull/1241 +* remove duplicate command #1228 by @camfairchild in https://github.com/opentensor/bittensor/pull/1231 +* test_forward_priority_2nd_request_timeout fix by @isabella618033 in https://github.com/opentensor/bittensor/pull/1276 +* Remove btcli query and btcli set_weights by @camfairchild in https://github.com/opentensor/bittensor/pull/1144 +* Merge releases 4.0.0 and 4.0.1 back to staging by @camfairchild in https://github.com/opentensor/bittensor/pull/1306 +* Improve development workflow documentation by @quac88 in https://github.com/opentensor/bittensor/pull/1262 +* staging updates and fixes by @ifrit98 in https://github.com/opentensor/bittensor/pull/1540 +* Add root get_weights command to btcli by @Rubberbandits in https://github.com/opentensor/bittensor/pull/1536 +* Fix typo by @steffencruz in https://github.com/opentensor/bittensor/pull/1543 +* remove duplicated debug message in dendrite by @ifrit98 in https://github.com/opentensor/bittensor/pull/1544 +* Cli fix by @ifrit98 in https://github.com/opentensor/bittensor/pull/1541 +* update faucet helpstr by @ifrit98 in https://github.com/opentensor/bittensor/pull/1542 +* Added mechanism to sum all delegated tao by @shibshib in https://github.com/opentensor/bittensor/pull/1547 +* Dict hash fix by @ifrit98 in https://github.com/opentensor/bittensor/pull/1548 +* Release/6.1.0 by @ifrit98 in https://github.com/opentensor/bittensor/pull/1550 +* Merge master by @ifrit98 in https://github.com/opentensor/bittensor/pull/1552 +* Streaming fix by @ifrit98 in https://github.com/opentensor/bittensor/pull/1551 +* Fix typos by @omahs in https://github.com/opentensor/bittensor/pull/1553 +* Normalize weights in r get weights table by @camfairchild in https://github.com/opentensor/bittensor/pull/1556 +* Dendrite & Synapse updates and fixes by @ifrit98 in https://github.com/opentensor/bittensor/pull/1555 +* rm root flag in metagraph by @ifrit98 in https://github.com/opentensor/bittensor/pull/1558 +* Max Faucet Runs == 3 by @ifrit98 in https://github.com/opentensor/bittensor/pull/1560 +* replace unknown wallet params (chain mismatch) with key values by @ifrit98 in https://github.com/opentensor/bittensor/pull/1559 +* Remove PoW registration cli and associated extrinsic by @ifrit98 in https://github.com/opentensor/bittensor/pull/1557 +* Add btcli wallet balance by @ifrit98 in https://github.com/opentensor/bittensor/pull/1564 +* Dendrite fixes by @ifrit98 in https://github.com/opentensor/bittensor/pull/1561 +* Release/6.2.0 by @ifrit98 in https://github.com/opentensor/bittensor/pull/1567 +* Master into staging by @ifrit98 in https://github.com/opentensor/bittensor/pull/1570 +* adding logging.exception by @surcyf123 in https://github.com/opentensor/bittensor/pull/1569 +* Update network.py by @wildcommunist in https://github.com/opentensor/bittensor/pull/1568 +* Subtensor Registry by @Eugene-hu in https://github.com/opentensor/bittensor/pull/1562 +* add instructions for upgrading bittensor with outdated version check by @ifrit98 in https://github.com/opentensor/bittensor/pull/1571 +* Add identity commands to btcli by @ifrit98 in https://github.com/opentensor/bittensor/pull/1566 +* Add set_delegate_take command to btcli by @Rubberbandits in https://github.com/opentensor/bittensor/pull/1563 +* Subtensor archive by @ifrit98 in https://github.com/opentensor/bittensor/pull/1575 +* Bugfix/list delegates by @ifrit98 in https://github.com/opentensor/bittensor/pull/1577 +* don't return result twice in query() by @ifrit98 in https://github.com/opentensor/bittensor/pull/1574 +* rename logging.py so doesn't circ import by @ifrit98 in https://github.com/opentensor/bittensor/pull/1572 +* add AxonInfo._string() by @ifrit98 in https://github.com/opentensor/bittensor/pull/1565 +* don't print __is_set for recursive objects by @ifrit98 in https://github.com/opentensor/bittensor/pull/1573 + +## New Contributors +* @omahs made their first contribution in https://github.com/opentensor/bittensor/pull/1553 +* @surcyf123 made their first contribution in https://github.com/opentensor/bittensor/pull/1569 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v6.0.1...v6.3.0 + + +## 6.2.0 / 2023-10-30 + +## What's Changed +* (un)Staking multiple avoid tx limit by @camfairchild in https://github.com/opentensor/bittensor/pull/1244 +* additional logging for prometheus by @Eugene-hu in https://github.com/opentensor/bittensor/pull/1246 +* Dataset fix by @isabella618033 in https://github.com/opentensor/bittensor/pull/1249 +* Grab delegates details from GitHub by @camfairchild in https://github.com/opentensor/bittensor/pull/1245 +* Add raw spec for local test and new bins by @camfairchild in https://github.com/opentensor/bittensor/pull/1243 +* Fix list_delegates on non-archive nodes by @camfairchild in https://github.com/opentensor/bittensor/pull/1232 +* Blacklist fixes + depreciation of old signatures by @Eugene-hu in https://github.com/opentensor/bittensor/pull/1240 +* [BIT-636] Change u16 weight normalization to max-upscaling by @opentaco in https://github.com/opentensor/bittensor/pull/1241 +* remove duplicate command #1228 by @camfairchild in https://github.com/opentensor/bittensor/pull/1231 +* test_forward_priority_2nd_request_timeout fix by @isabella618033 in https://github.com/opentensor/bittensor/pull/1276 +* Remove btcli query and btcli set_weights by @camfairchild in https://github.com/opentensor/bittensor/pull/1144 +* Merge releases 4.0.0 and 4.0.1 back to staging by @camfairchild in https://github.com/opentensor/bittensor/pull/1306 +* Improve development workflow documentation by @quac88 in https://github.com/opentensor/bittensor/pull/1262 +* staging updates and fixes by @ifrit98 in https://github.com/opentensor/bittensor/pull/1540 +* Add root get_weights command to btcli by @Rubberbandits in https://github.com/opentensor/bittensor/pull/1536 +* Fix typo by @steffencruz in https://github.com/opentensor/bittensor/pull/1543 +* remove duplicated debug message in dendrite by @ifrit98 in https://github.com/opentensor/bittensor/pull/1544 +* Cli fix by @ifrit98 in https://github.com/opentensor/bittensor/pull/1541 +* update faucet helpstr by @ifrit98 in https://github.com/opentensor/bittensor/pull/1542 +* Added mechanism to sum all delegated tao by @shibshib in https://github.com/opentensor/bittensor/pull/1547 +* Dict hash fix by @ifrit98 in https://github.com/opentensor/bittensor/pull/1548 +* Release/6.1.0 by @ifrit98 in https://github.com/opentensor/bittensor/pull/1550 +* Merge master by @ifrit98 in https://github.com/opentensor/bittensor/pull/1552 +* Streaming fix by @ifrit98 in https://github.com/opentensor/bittensor/pull/1551 +* Fix typos by @omahs in https://github.com/opentensor/bittensor/pull/1553 +* Normalize weights in r get weights table by @camfairchild in https://github.com/opentensor/bittensor/pull/1556 +* Dendrite & Synapse updates and fixes by @ifrit98 in https://github.com/opentensor/bittensor/pull/1555 +* rm root flag in metagraph by @ifrit98 in https://github.com/opentensor/bittensor/pull/1558 +* Max Faucet Runs == 3 by @ifrit98 in https://github.com/opentensor/bittensor/pull/1560 +* replace unknown wallet params (chain mismatch) with key values by @ifrit98 in https://github.com/opentensor/bittensor/pull/1559 +* Remove PoW registration cli and associated extrinsic by @ifrit98 in https://github.com/opentensor/bittensor/pull/1557 +* Add btcli wallet balance by @ifrit98 in https://github.com/opentensor/bittensor/pull/1564 +* Dendrite fixes by @ifrit98 in https://github.com/opentensor/bittensor/pull/1561 + +## New Contributors +* @omahs made their first contribution in https://github.com/opentensor/bittensor/pull/1553 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v6.0.1...v6.2.0 + + +## 6.1.0 / 2023-10-17 + +## What's Changed +* (un)Staking multiple avoid tx limit by @camfairchild in https://github.com/opentensor/bittensor/pull/1244 +* additional logging for prometheus by @Eugene-hu in https://github.com/opentensor/bittensor/pull/1246 +* Dataset fix by @isabella618033 in https://github.com/opentensor/bittensor/pull/1249 +* Grab delegates details from GitHub by @camfairchild in https://github.com/opentensor/bittensor/pull/1245 +* Add raw spec for local test and new bins by @camfairchild in https://github.com/opentensor/bittensor/pull/1243 +* Fix list_delegates on non-archive nodes by @camfairchild in https://github.com/opentensor/bittensor/pull/1232 +* Blacklist fixes + depreciation of old signatures by @Eugene-hu in https://github.com/opentensor/bittensor/pull/1240 +* [BIT-636] Change u16 weight normalization to max-upscaling by @opentaco in https://github.com/opentensor/bittensor/pull/1241 +* remove duplicate command #1228 by @camfairchild in https://github.com/opentensor/bittensor/pull/1231 +* test_forward_priority_2nd_request_timeout fix by @isabella618033 in https://github.com/opentensor/bittensor/pull/1276 +* Remove btcli query and btcli set_weights by @camfairchild in https://github.com/opentensor/bittensor/pull/1144 +* Merge releases 4.0.0 and 4.0.1 back to staging by @camfairchild in https://github.com/opentensor/bittensor/pull/1306 +* Improve development workflow documentation by @quac88 in https://github.com/opentensor/bittensor/pull/1262 +* staging updates and fixes by @ifrit98 in https://github.com/opentensor/bittensor/pull/1540 +* Add root get_weights command to btcli by @Rubberbandits in https://github.com/opentensor/bittensor/pull/1536 +* Fix typo by @steffencruz in https://github.com/opentensor/bittensor/pull/1543 +* remove duplicated debug message in dendrite by @ifrit98 in https://github.com/opentensor/bittensor/pull/1544 +* Cli fix by @ifrit98 in https://github.com/opentensor/bittensor/pull/1541 +* update faucet helpstr by @ifrit98 in https://github.com/opentensor/bittensor/pull/1542 +* Added mechanism to sum all delegated tao by @shibshib in https://github.com/opentensor/bittensor/pull/1547 +* Dict hash fix by @ifrit98 in https://github.com/opentensor/bittensor/pull/1548 + + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v6.0.1...v6.1.0 + + +## 6.0.1 / 2023-10-02 + +## What's Changed +* Fix requirements/prod.txt, we had a bad format dependency not allowed by PyPi by @eduardogr in https://github.com/opentensor/bittensor/pull/1537 + + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v6.0.0...v6.0.1 + + +## 6.0.0 / 2023-10-02 + +## What's Changed +* - Adjusted blacklist argument default to False by @Inquinim in https://github.com/opentensor/bittensor/pull/1448 +* Release/5.3.1 by @camfairchild in https://github.com/opentensor/bittensor/pull/1444 +* Release/5.3.2 by @ifrit98 in https://github.com/opentensor/bittensor/pull/1462 +* [hotfix] Release v5.3.3 by @camfairchild in https://github.com/opentensor/bittensor/pull/1467 +* Update README.md by @unconst in https://github.com/opentensor/bittensor/pull/1477 +* Release/5.3.4 by @ifrit98 in https://github.com/opentensor/bittensor/pull/1483 +* Revolution by @unconst in https://github.com/opentensor/bittensor/pull/1450 + + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v5.3.0...v6.0.0 + + +## 6.0.1 / 2023-10-02 + +## What's Changed +* Fix requirements/prod.txt, we had a bad format dependency not allowed by PyPi by @eduardogr in https://github.com/opentensor/bittensor/pull/1537 + + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v6.0.0...v6.0.1 + + +## 5.3.4 / 2023-08-16 + +# What's Changed +* Removes miniupnpc by @ifrit98 (completely unused and requires a sudo install) +* Fixes blacklist vpermit_required by @inquinim e80d3d5 +* Add try/except and timeout to version checking with exception handles by @ifrit98 a6a89fd +* Further updates CONTRIBUTING.md and DEVELOPMENT_WORKFLOW.md by @gitphantomman 3fefdbb +* Adds automatic compatibility checks to circleci for all major python3 supported versions. add checks by @ifrit98 #1484 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v5.3.3...v5.3.4 + + +## 5.3.3 / 2023-07-26 + +## What's Changed +* Remove datasets requirement by @camfairchild in 2eabf0002b01 +* Relax bittensor-* requirements by @camfairchild in da9300ba5b2 + + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v5.3.2...v5.3.3 + + +## 5.3.2 / 2023-07-25 + +## What's Changed +* Btlm miner by @shibshib in https://github.com/opentensor/bittensor/pull/1463 +* Don't finalize set_weights ext by @camfairchild in https://github.com/opentensor/bittensor/pull/1461 +* Faster overview pull by @camfairchild in https://github.com/opentensor/bittensor/pull/1464 +* Contrib revamp by @ifrit98 in https://github.com/opentensor/bittensor/pull/1456 +* fix torch typehint on some neurons BT-1329 by @camfairchild in https://github.com/opentensor/bittensor/pull/1460 +* bump bittensor-wallet version to 0.0.5 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v5.3.1...v5.3.2 + + +## 5.3.1 / 2023-07-06 + + ## What's Changed + * bump bittensor-wallet req, update cryptography security req by @@ifrit98 in [91d13b0](https://github.com/opentensor/bittensor/commit/91d13b0fa711621cbf823708d4368b1b387e42c4) + * Fixes Discord Link Issue #1442 by @camfairchild in [54d6248](https://github.com/opentensor/bittensor/commit/54d62487d4cb59e0b5edcd53acdca013108d155b) + * move mocks to bittensor_wallet package by @camfairchild in https://github.com/opentensor/bittensor/pull/1441 + * Bump bittensor-wallet version to 0.0.4 + + **Full Changelog**: https://github.com/opentensor/bittensor/compare/v5.3.0...v5.3.1 + +## 5.3.0 / 2023-07-04 + +## What's Changed +* [BIT-351] Ask for wallet name on btcli unstake by @camfairchild in https://github.com/opentensor/bittensor/pull/1387 +* Fix tests using pure-Python MockSubtensor by @camfairchild in https://github.com/opentensor/bittensor/pull/1349 +* Update README.md by @mostimasblunderbuss in https://github.com/opentensor/bittensor/pull/1397 +* Update authint version by @ifrit98 in https://github.com/opentensor/bittensor/pull/1395 +* Fix subtensor factory integration test by @camfairchild in https://github.com/opentensor/bittensor/pull/1400 +* Remove neurons by @ifrit98 in https://github.com/opentensor/bittensor/pull/1389 +* Merge pull request #1394 from opentensor/fix_axon_requests by @ifrit98 in https://github.com/opentensor/bittensor/pull/1406 +* remove hotkey from proto and dendrite by @ifrit98 in https://github.com/opentensor/bittensor/pull/1407 +* Weight Utils fix by @mrseeker in https://github.com/opentensor/bittensor/pull/1372 +* Extract config to new package by @camfairchild in https://github.com/opentensor/bittensor/pull/1401 +* Extract wallet by @camfairchild in https://github.com/opentensor/bittensor/pull/1403 +* BTCli integration with new governance protocol by @Rubberbandits in https://github.com/opentensor/bittensor/pull/1398 +* Reverting unnecessary commits for next release. by @camfairchild in https://github.com/opentensor/bittensor/pull/1415 +* Extract wallet and config by @camfairchild in https://github.com/opentensor/bittensor/pull/1411 + +## New Contributors +* @mostimasblunderbuss made their first contribution in https://github.com/opentensor/bittensor/pull/1397 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v5.2.0...v5.3.0 + + +## 5.2.0 / 2023-06-28 + +## What's Changed +* add default 1024 max stake limit for querying UIDs with vpermit. by @ifrit98 in https://github.com/opentensor/bittensor/pull/1379 +* Fixes validator permit issue seen on master by @unconst in https://github.com/opentensor/bittensor/pull/1381 +* Added conda environment by @shibshib in https://github.com/opentensor/bittensor/pull/1386 +* Update package requirements (hotfix) by @ifrit98 in https://github.com/opentensor/bittensor/pull/1385 +* Merge master into new_staging by @ifrit98 in https://github.com/opentensor/bittensor/pull/1388 +* Fix axon requests signature using metadata by @unconst in https://github.com/opentensor/bittensor/pull/1394 +* Governance Protocol Release by @Rubberbandits in https://github.com/opentensor/bittensor/pull/1414 + + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v5.1.0...v5.2.0 + + +## 5.1.0 / 2023-05-30 + +## What's Changed +* update readme by @unconst in https://github.com/opentensor/bittensor/pull/1344 +* Reset scores for validators by @adriansmares in https://github.com/opentensor/bittensor/pull/1359 + + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v5.0.0...v5.1.0 + + +## 5.0.0 / 2023-05-17 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v4.0.1...v5.0.0 + + +## 4.0.1 / 2023-04-21 + +* Fix btcli my_delegates bug by @camfairchild in ef32a4da0d0827ab5977af1454d66ffe97cbc572 +* Fix endpoint protocol check bug by @camfairchild and @Eugene-hu in https://github.com/opentensor/bittensor/pull/1296 +* Fix changelog script and perms by @camfairchild in f5e7f1e9e9717d229fdec6875fdb9a3051c4bd6b and 1aed09a162ef0fe4d9def2faf261b15dc4c1fa8d + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v4.0.0...v4.0.1 + + +## 4.0.0 / 2023-04-20 + +## What's Changed +* add mnrv-ai to delegates.json by @SFuller4 in https://github.com/opentensor/bittensor/pull/1226 +* Update delegates list by @adriansmares in https://github.com/opentensor/bittensor/pull/1225 +* Update delegates.json by @whiterhinoTAO in https://github.com/opentensor/bittensor/pull/1230 +* Hotfix - Cli unstake fix by @Eugene-hu in https://github.com/opentensor/bittensor/pull/1233 +* Fix permissions for release github script by @eduardogr in https://github.com/opentensor/bittensor/pull/1224 +* Staging into Release branch by @camfairchild in https://github.com/opentensor/bittensor/pull/1275 +* Remove codecov by @camfairchild in https://github.com/opentensor/bittensor/pull/1282 +* Use alt new preseal by @camfairchild in https://github.com/opentensor/bittensor/pull/1269 + +## New Contributors +* @SFuller4 made their first contribution in https://github.com/opentensor/bittensor/pull/1226 +* @whiterhinoTAO made their first contribution in https://github.com/opentensor/bittensor/pull/1230 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v3.7.0...v4.0.0 + + +## 3.6.3 / 2023-01-21 + +## What's Changed +* [hotfix][3.6.3] Fixing no version checking by @eduardogr in https://github.com/opentensor/bittensor/pull/1063 + + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v3.6.2...v3.6.3 + + +## 3.6.2 / 2023-01-19 + +## What's Changed +* Hotfix/3.6.2/validator logit parameters by @Eugene-hu in https://github.com/opentensor/bittensor/pull/1057 + + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v3.6.1...v3.6.2 + + +## 3.6.1 / 2022-12-21 + +## What's Changed +* V3.6.0 nobunaga merge by @Eugene-hu in https://github.com/opentensor/bittensor/pull/1028 +* Integration dendrite test fixes by @Eugene-hu in https://github.com/opentensor/bittensor/pull/1029 +* Adding 3.6.0 release notes to CHANGELOG by @eduardogr in https://github.com/opentensor/bittensor/pull/1032 +* [BIT-612] Validator robustness improvements by @opentaco in https://github.com/opentensor/bittensor/pull/1034 +* [Hotfix 3.6.1] Validator robustness by @opentaco in https://github.com/opentensor/bittensor/pull/1035 + + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v3.6.0...v3.6.1 + + +## 3.6.0 / 2022-12-13 + +## What's Changed +* Removal of dendrite multiprocessing by @Eugene-hu in https://github.com/opentensor/bittensor/pull/1017 +* Merging back 3.5.1 fix to nobunaga by @eduardogr in https://github.com/opentensor/bittensor/pull/1018 +* Release/3.5.0 post release by @eduardogr in https://github.com/opentensor/bittensor/pull/1010 +* Fixes issue with --neuron.no_set_weights by @camfairchild in https://github.com/opentensor/bittensor/pull/1020 +* Removing GitHub workflow push docker by @eduardogr in https://github.com/opentensor/bittensor/pull/1011 +* [Fix] fix max stake for single by @camfairchild in https://github.com/opentensor/bittensor/pull/996 +* [Feature] mention balance if not no prompt by @camfairchild in https://github.com/opentensor/bittensor/pull/995 +* Add signature v2 format by @adriansmares in https://github.com/opentensor/bittensor/pull/983 +* Improving the way we manage requirements by @eduardogr in https://github.com/opentensor/bittensor/pull/1003 +* [BIT-601] Scaling law on EMA loss by @opentaco in https://github.com/opentensor/bittensor/pull/1022 +* [BIT-602] Update scaling power from subtensor by @opentaco in https://github.com/opentensor/bittensor/pull/1027 +* Release 3.6.0 by @eduardogr in https://github.com/opentensor/bittensor/pull/1023 + +## New Contributors +* @adriansmares made their first contribution in https://github.com/opentensor/bittensor/pull/976 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v3.5.1...v3.6.0 + + +## 3.5.1 / 2022-11-24 + +## What's Changed +* [hotfix] pin scalecodec lower by @camfairchild in https://github.com/opentensor/bittensor/pull/1013 + + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v3.5.0...v3.5.1 + +## 3.5.0 / 2022-11-24 + +## What's Changed + +- [Fix] allow synapse all (https://github.com/opentensor/bittensor/pull/988) + - allow set synapse All using flag + - add test + - use dot get + +- [Feature] Mark registration threads as daemons (https://github.com/opentensor/bittensor/pull/998) + - make solver processes daemons + +- [Feature] Validator debug response table (https://github.com/opentensor/bittensor/pull/999) + - Add response table to validator debugging + +- [Feature] Validator weight setting improvements (https://github.com/opentensor/bittensor/pull/1000) + - Remove responsive prioritization from validator weight calculation + - Move metagraph_sync just before weight setting + - Add metagraph register to validator + - Update validator epoch conditions + - Log epoch while condition details + - Consume validator nucleus UID queue fully + - Increase synergy table display precision + - Round before casting to int in phrase_cross_entropy +- small fix for changelog and version by @Eugene-hu in https://github.com/opentensor/bittensor/pull/993 +- release/3.5.0 by @eduardogr in https://github.com/opentensor/bittensor/pull/1006 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v3.4.3...v3.5.0 + + +## 3.4.3 / 2022-11-15 + +## What's Changed +* [Hotfix] Synapse security update by @opentaco in https://github.com/opentensor/bittensor/pull/991 + + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v3.4.2...v3.4.3 + +## 3.4.2 / 2022-11-09 + +## What's Changed +* Adding 3.4.0 changelog to CHANGELOG.md by @eduardogr in https://github.com/opentensor/bittensor/pull/953 +* Release 3.4.2 by @unconst in https://github.com/opentensor/bittensor/pull/970 + + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v3.4.1...v3.4.2 + +## 3.4.1 / 2022-10-13 + +## What's Changed +* [Hotfix] Fix CUDA Reg update block by @camfairchild in https://github.com/opentensor/bittensor/pull/954 + + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v3.4.0...v3.4.1 + +## 3.4.0 / 2022-10-13 + +## What's Changed +* Parameters update by @Eugene-hu #936 +* Bittensor Generate by @unconst #941 +* Prometheus by @unconst #928 +* [Tooling][Release] Adding release script by @eduardogr in https://github.com/opentensor/bittensor/pull/948 + + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v3.3.4...v3.4.0 + + +## 3.3.4 / 2022-10-03 + +### What's Changed +* [hot-fix] fix indent again. add test by @camfairchild in https://github.com/opentensor/bittensor/pull/907 +* Delete old gitbooks by @quac88 in https://github.com/opentensor/bittensor/pull/924 +* Release/3.3.4 by @Eugene-hu in https://github.com/opentensor/bittensor/pull/927 + +### New Contributors +* @quac88 made their first contribution in https://github.com/opentensor/bittensor/pull/924 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v3.3.3...v3.3.4 + + +## 3.3.3 / 2022-09-06 + +### What's Changed +* [feature] cpu register faster by @camfairchild in https://github.com/opentensor/bittensor/pull/854 +* [hotfix] fix flags for multiproc register limit by @camfairchild in https://github.com/opentensor/bittensor/pull/876 +* Fix/diff unpack bit shift by @camfairchild in https://github.com/opentensor/bittensor/pull/878 +* [Feature] [cubit] CUDA registration solver by @camfairchild in https://github.com/opentensor/bittensor/pull/868 +* Fix/move overview args to cli by @camfairchild in https://github.com/opentensor/bittensor/pull/867 +* Add/address CUDA reg changes by @camfairchild in https://github.com/opentensor/bittensor/pull/879 +* [Fix] --help command by @camfairchild in https://github.com/opentensor/bittensor/pull/884 +* Validator hotfix min allowed weights by @Eugene-hu in https://github.com/opentensor/bittensor/pull/885 +* [BIT-552] Validator improvements (nucleus permute, synergy avg) by @opentaco in https://github.com/opentensor/bittensor/pull/889 +* Bit 553 bug fixes by @isabella618033 in https://github.com/opentensor/bittensor/pull/886 +* add check to add ws:// if needed by @camfairchild in https://github.com/opentensor/bittensor/pull/896 +* [BIT-572] Exclude lowest quantile from weight setting by @opentaco in https://github.com/opentensor/bittensor/pull/895 +* [BIT-573] Improve validator epoch and responsives handling by @opentaco in https://github.com/opentensor/bittensor/pull/901 +* Nobunaga Release V3.3.3 by @Eugene-hu in https://github.com/opentensor/bittensor/pull/899 + + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v3.3.2...v3.3.3 + +## 3.3.2 / 2022-08-18 + +### SynapseType fix in dendrite +### What's Changed +* SynapseType fix in dendrite by @robertalanm in https://github.com/opentensor/bittensor/pull/874 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v3.3.1...v3.3.2 + +## 3.3.1 / 2022-08-17 + +### What's Changed +* [hotfix] Fix GPU reg bug. bad indent by @camfairchild in https://github.com/opentensor/bittensor/pull/883 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v3.3.0...v3.3.1 + +## 3.3.0 / 2022-08-16 + +### CUDA registration +This release adds the ability to complete the registration using a CUDA-capable device. +See https://github.com/opentensor/cubit/releases/tag/v1.0.5 for the required `cubit` v1.0.5 release + +Also a few bug fixes for the CLI + +### What's Changed +* [hotfix] fix flags for run command, fix hotkeys flag for overview, and [feature] CUDA reg by @camfairchild in https://github.com/opentensor/bittensor/pull/877 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v3.2.0...v3.3.0 + +## 3.2.0 / 2022-08-12 + +### Validator saving and responsive-priority weight-setting + +### What's Changed +* [BIT-540] Choose responsive UIDs for setting weights in validator + validator save/load by @opentaco in https://github.com/opentensor/bittensor/pull/872 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v3.1.0...v3.2.0 + +## 3.1.0 / 2022-08-11 + +### Optimizing multi-processed CPU registration +This release refactors the registration code for CPU registration to improve solving performance. + +### What's Changed +* [feature] cpu register faster (#854) by @camfairchild in https://github.com/opentensor/bittensor/pull/875 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v3.0.0...v3.1.0 + +## 3.0.0 / 2022-08-08 + +### Synapse update + +## diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000..03c0533746 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,40 @@ +# syntax=docker/dockerfile:1 +FROM python:3.11.8-bookworm + +LABEL bittensor.image.authors="bittensor.com" \ + bittensor.image.vendor="Bittensor" \ + bittensor.image.title="bittensor/bittensor" \ + bittensor.image.description="Bittensor: Incentivized Peer to Peer Neural Networks" \ + bittensor.image.source="https://github.com/opentensor/bittensor.git" \ + bittensor.image.revision="${VCS_REF}" \ + bittensor.image.created="${BUILD_DATE}" \ + bittensor.image.documentation="https://app.gitbook.com/@opentensor/s/bittensor/" +ARG DEBIAN_FRONTEND=noninteractive + +# Update the base image +RUN apt-get update && apt-get upgrade -y +# Install bittensor +## Install dependencies +RUN apt-get install -y curl sudo nano git htop netcat-openbsd wget unzip tmux apt-utils cmake build-essential +## Upgrade pip +RUN pip3 install --upgrade pip + +# Install nvm and pm2 +RUN curl -o install_nvm.sh https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh && \ + echo 'fabc489b39a5e9c999c7cab4d281cdbbcbad10ec2f8b9a7f7144ad701b6bfdc7 install_nvm.sh' | sha256sum --check && \ + bash install_nvm.sh + +RUN bash -c "source $HOME/.nvm/nvm.sh && \ + # use node 16 + nvm install 16 && \ + # install pm2 + npm install --location=global pm2" + +RUN mkdir -p /root/.bittensor/bittensor +COPY . /root/.bittensor/bittensor +RUN cd /root/.bittensor/bittensor && python3 -m pip install . + +# Increase ulimit to 1,000,000 +RUN prlimit --pid=$PPID --nofile=1000000 + +EXPOSE 8091 diff --git a/LICENSE b/LICENSE index d16dd53259..8d10866d56 100644 --- a/LICENSE +++ b/LICENSE @@ -1,20 +1,16 @@ -Copyright (c) 2018 The Python Packaging Authority +The MIT License (MIT) +Copyright © 2021 Yuma Rao -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +documentation files (the “Software”), to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +The above copyright notice and this permission notice shall be included in all copies or substantial portions of +the Software. +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000000..d68152d42d --- /dev/null +++ b/Makefile @@ -0,0 +1,26 @@ +SHELL := /bin/bash +.PHONY: init-venv clean-venv clean install install-dev reinstall reinstall-dev + +init-venv: + python3 -m venv venv && source ./venv/bin/activate + +clean-venv: + source ./venv/bin/activate && \ + pip freeze > make_venv_to_uninstall.txt && \ + pip uninstall -r make_venv_to_uninstall.txt -y && \ + rm make_venv_to_uninstall.txt + +clean: + rm -rf dist/ build/ bittensor.egg-info/ .pytest_cache/ lib/ + +install: init-venv + source ./venv/bin/activate && \ + python3 -m pip install . + +install-dev: init-venv + source ./venv/bin/activate && \ + python3 -m pip install -e '.[dev]' + +reinstall: clean clean-venv install + +reinstall-dev: clean clean-venv install-dev diff --git a/README.md b/README.md index 5dba6c6164..e74a7e1490 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,303 @@ -# Bittensor -[![Build status](https://circleci.com/gh/opentensor/bittensor.svg?style=shield)](https://circleci.com/gh/opentensor/bittensor) -[![Documentation Status](https://readthedocs.org/projects/bittensor-docs/badge/?version=latest)](https://bittensor-docs.readthedocs.io/en/latest/?badge=latest) -[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +
-Neural networks which mine crypto by producing information for their peers. +# **Bittensor SDK** +[![Discord Chat](https://img.shields.io/discord/308323056592486420.svg)](https://discord.gg/bittensor) +[![CodeQL](https://github.com/opentensor/bittensor/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/opentensor/bittensor/actions) +[![PyPI version](https://badge.fury.io/py/bittensor.svg)](https://badge.fury.io/py/bittensor) +[![Codecov](https://codecov.io/gh/opentensor/bittensor/graph/badge.svg)](https://app.codecov.io/gh/opentensor/bittensor) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -## Links -- [Documentation](https://bittensor-docs.readthedocs.io/en/latest/index.html) -- [Installation](https://bittensor-docs.readthedocs.io/en/latest/getting-started/installation.html) -- [Getting Started](https://bittensor-docs.readthedocs.io/en/latest/getting-started/run-multiple-bittensor-instances.html) -- [Architecture](https://bittensor-docs.readthedocs.io/en/latest/bittensor-deep-dive/bittensor-architecture.html) +--- -## Acknowledgments -**learning-at-home/hivemind**: +## Internet-scale Neural Networks + +[Discord](https://discord.gg/qasY3HA9F9) • [Network](https://taostats.io/) • [Research](https://bittensor.com/whitepaper) • [Documentation](https://docs.bittensor.com) + +
+ +- [Overview of Bittensor](#overview-of-bittensor) +- [The Bittensor SDK](#the-bittensor-sdk) +- [Is Bittensor a blockchain or an AI platform?](#is-bittensor-a-blockchain-or-an-ai-platform) +- [Subnets](#subnets) +- [Subnet validators and subnet miners](#subnet-validators-and-subnet-miners) +- [Yuma Consensus](#yuma-consensus) +- [Release Notes](#release-notes) +- [Install Bittensor SDK](#install-bittensor-sdk) +- [Upgrade](#upgrade) +- [Install on macOS and Linux](#install-on-macos-and-linux) + - [Install using a Bash command](#install-using-a-bash-command) + - [Install using `pip3 install`](#install-using-pip3-install) + - [Install from source](#install-from-source) + - [Verify using Python interpreter](#verify-using-python-interpreter) + - [Verify by listing axon information](#verify-by-listing-axon-information) +- [Release Guidelines](#release-guidelines) +- [Contributions](#contributions) +- [License](#license) +- [Acknowledgments](#acknowledgments) + +--- + +## Overview of Bittensor + +Welcome! Bittensor is an open source platform on which you can produce competitive digital commodities. These digital commodities can be machine intelligence, storage space, compute power, protein folding, financial markets prediction, and many more. You are rewarded in **TAO** when you produce best digital commodities. + +## The Bittensor SDK + +The Opentensor Foundation (OTF) provides all the open source tools, including this Bittensor SDK, the codebase and the documentation, with step-by-step tutorials and guides, to enable you to participate in the Bittensor ecosystem. + +- **Developer documentation**: https://docs.bittensor.com. +- **A Beginner's Q and A on Bittensor**: https://docs.bittensor.com/questions-and-answers. +- **Bittensor whitepaper**: https://bittensor.com/whitepaper. + +This Bittensor SDK contains ready-to-use Python packages for interacting with the Bittensor ecosystem, writing subnet incentive mechanisms, subnet miners, subnet validators and querying the subtensor (the blockchain part of the Bittensor network). + +--- + +## Is Bittensor a blockchain or an AI platform? + +In Bittensor there is one blockchain, and many platforms that are connected to this one blockchain. We call these platforms as **subnets**, and this one blockchain **subtensor**. So, a subnet can be AI-related or it can be something else. The Bittensor network has a number of distinct subnets. All these subnets interact with subtensor blockchain. If you are thinking, "So, subnets are not part of the blockchain but only interact with it?" then the answer is "yes, exactly." + +## Subnets + +Each category of the digital commodity is produced in a distinct subnet. Applications are built on these specific subnets. End-users of these applications would be served by these applications. + +## Subnet validators and subnet miners + +Subnets, which exist outside the blockchain and are connected to it, are off-chain competitions where only the best producers are rewarded. A subnet consists of off-chain **subnet validators** who initiate the competition for a specific digital commodity, and off-chain **subnet miners** who compete and respond by producing the best quality digital commodity. + +## Yuma Consensus + +Scores are assigned to the top-performing subnet miners and subnet validators. The on-chain Yuma Consensus determines the TAO rewards for these top performers. The Bittensor blockchain, the subtensor, runs on decentralized validation nodes, just like any blockchain. + +**This SDK repo is for Bittensor platform only** +This Bittensor SDK codebase is for the Bittensor platform only, designed to help developers create subnets and build tools on Bittensor. For subnets and applications, refer to subnet-specific websites, which are maintained by subnet owners. + +## Release Notes + +See [Bittensor SDK Release Notes](https://docs.bittensor.com/bittensor-rel-notes). + +--- + +## Install Bittensor SDK + +Before you can start developing, you must install Bittensor SDK and then create Bittensor wallet. + +## Upgrade + +If you already installed Bittensor SDK, make sure you upgrade to the latest version. Run the below command: + +```bash +python3 -m pip install --upgrade bittensor +``` + +--- + +## Install on macOS and Linux + +### Note for macOS users +The macOS preinstalled CPython installation is compiled with LibreSSL instead of OpenSSL. There are a number +of issues with LibreSSL, and as such is not fully supported by the libraries used by bittensor. Thus we highly recommend, if +you are using a Mac, to first install Python from [Homebrew](https://brew.sh/). Additionally, the Rust FFI bindings +[if installing from precompiled wheels (default)] require the Homebrew-installed OpenSSL pacakge. If you choose to use +the preinstalled Python version from macOS, things may not work completely. + +### Installation +You can install Bittensor SDK on your local machine in either of the following ways. **Make sure you verify your installation after you install**: +- [Install using a Bash command](#install-using-a-bash-command). +- [Install using `pip3 install`](#install-using-pip3-install) +- [Install from source](#install-from-source) + +### Install using a Bash command + +This is the most straightforward method. It is recommended for a beginner as it will pre-install requirements like Python, if they are not already present on your machine. Copy and paste the following `bash` command into your terminal: + +```bash +/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/opentensor/bittensor/master/scripts/install.sh)" +``` + +**For Ubuntu-Linux users** +If you are using Ubuntu-Linux, the script will prompt for `sudo` access to install all required apt-get packages. + +### Install using `pip3 install` + +```bash +python3 -m venv bt_venv +source bt_venv/bin/activate +pip install bittensor +``` + +### Install from source + +1. Create and activate a virtual environment + + - Create Python virtual environment. Follow [this guide on python.org](https://docs.python.org/3/library/venv.html#creating-virtual-environments). + + - Activate the new environment. Follow [this guide on python.org](https://docs.python.org/3/library/venv.html#how-venvs-work) + +2. Clone the Bittensor SDK repo +```bash +git clone https://github.com/opentensor/bittensor.git +``` + +3. Install + +You can install using any of the below options: + +- **Install SDK**: Run the below command to install Bittensor SDK in the above virtual environment. This will also install `btcli`. + + ```python + pip install bittensor + ``` + +- **Install SDK with `torch`**: Install Bittensor SDK with [`torch`](https://pytorch.org/docs/stable/torch.html). + + ```python + pip install bittensor[torch] + ``` + In some environments the above command may fail, in which case run the command with added quotes as shown below: + + ```python + pip install "bittensor[torch]" + ``` + +- **Install SDK with `cubit`**: Install Bittensor SDK with [`cubit`](https://github.com/opentensor/cubit). + + 1. Install `cubit` first. See the [Install](https://github.com/opentensor/cubit?tab=readme-ov-file#install) section. **Only Python 3.9 and 3.10 versions are supported**. + 2. Then install SDK with `pip install bittensor`. + + +### Troubleshooting +#### SSL: CERTIFICATE_VERIFY_FAILED + +If you are encountering a `[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate` +error, use the command `python -m bittensor certifi` which will update your local SSL certificates. + +--- + +## Install on Windows + +To install and run Bittensor SDK on Windows you must install [**WSL 2** (Windows Subsystem for Linux)](https://learn.microsoft.com/en-us/windows/wsl/about) on Windows and select [Ubuntu Linux distribution](https://github.com/ubuntu/WSL/blob/main/docs/guides/install-ubuntu-wsl2.md). + +After you installed the above, follow the same installation steps described above in [Install on macOS and Linux](#install-on-macos-and-linux). + +**ALERT**: **Limited support on Windows** +While wallet transactions like delegating, transfer, registering, staking can be performed on a Windows machine using WSL 2, the mining and validating operations are not recommended and are not supported on Windows machines. + +--- + +## Verify the installation + +You can verify your installation in either of the below ways: + +### Verify using `btsdk` version + +```bash +python3 -m bittensor +``` + +The above command will show you the version of the `btsdk` you just installed. + +### Verify using Python interpreter + +1. Launch the Python interpreter on your terminal. + + ```bash + python3 + ``` +2. Enter the following two lines in the Python interpreter. + + ```python + import bittensor as bt + print( bt.__version__ ) + ``` + The Python interpreter output will look like below: + + ```python + Python 3.11.6 (main, Oct 2 2023, 13:45:54) [Clang 15.0.0 (clang-1500.0.40.1)] on darwin + Type "help", "copyright", "credits" or "license" for more information. + >>> import bittensor as bt + >>> print( bt.__version__ ) + + ``` +You will see the version number you installed in place of ``. + +### Verify by listing axon information + +You can also verify the Bittensor SDK installation by listing the axon information for the neurons. Enter the following lines in the Python interpreter. + +```python +import bittensor +metagraph = bittensor.Metagraph(1) +metagraph.axons[:10] +``` +The Python interpreter output will look like below. + +```bash +[AxonInfo( /ipv4/3.139.80.241:11055, 5GqDsK6SAPyQtG243hbaKTsoeumjQQLhUu8GyrXikPTmxjn7, 5D7u5BTqF3j1XHnizp9oR67GFRr8fBEFhbdnuVQEx91vpfB5, 600 ), AxonInfo( /ipv4/8.222.132.190:5108, 5CwqDkDt1uk2Bngvf8avrapUshGmiUvYZjYa7bfA9Gv9kn1i, 5HQ9eTDorvovKTxBc9RUD22FZHZzpy1KRfaxCnRsT9QhuvR6, 600 ), AxonInfo( /ipv4/34.90.71.181:8091, 5HEo565WAy4Dbq3Sv271SAi7syBSofyfhhwRNjFNSM2gP9M2, 5ChuGqW2cxc5AZJ29z6vyTkTncg75L9ovfp8QN8eB8niSD75, 601 ), AxonInfo( /ipv4/64.247.206.79:8091, 5HK5tp6t2S59DywmHRWPBVJeJ86T61KjurYqeooqj8sREpeN, 5E7W9QXNoW7se7B11vWRMKRCSWkkAu9EYotG5Ci2f9cqV8jn, 601 ), AxonInfo( /ipv4/51.91.30.166:40203, 5EXYcaCdnvnMZbozeknFWbj6aKXojfBi9jUpJYHea68j4q1a, 5CsxoeDvWsQFZJnDCyzxaNKgA8pBJGUJyE1DThH8xU25qUMg, 601 ), AxonInfo( /ipv4/149.137.225.62:8091, 5F4tQyWrhfGVcNhoqeiNsR6KjD4wMZ2kfhLj4oHYuyHbZAc3, 5Ccmf1dJKzGtXX7h17eN72MVMRsFwvYjPVmkXPUaapczECf6, 600 ), AxonInfo( /ipv4/38.147.83.11:8091, 5Hddm3iBFD2GLT5ik7LZnT3XJUnRnN8PoeCFgGQgawUVKNm8, 5DCQw11aUW7bozAKkB8tB5bHqAjiu4F6mVLZBdgJnk8dzUoV, 610 ), AxonInfo( /ipv4/38.147.83.30:41422, 5HNQURvmjjYhTSksi8Wfsw676b4owGwfLR2BFAQzG7H3HhYf, 5EZUTdAbXyLmrs3oiPvfCM19nG6oRs4X7zpgxG5oL1iK4MAh, 610 ), AxonInfo( /ipv4/54.227.25.215:10022, 5DxrZuW8kmkZPKGKp1RBVovaP5zHtPLDHYc5Yu82Z1fWqK5u, 5FhXUSmSZ2ec7ozRSA8Bg3ywmGwrjoLLzsXjNcwmZme2GcSC, 601 ), AxonInfo( /ipv4/52.8.243.76:40033, 5EnZN591jjsKKbt3yBtfGKWHxhxRH9cJonqTKRT5yTRUyNon, 5ChzhHyGmWwEdHjuvAxoUifHEZ6xpUjR67fDd4a42UrPysyB, 601 )] +>>> +``` + +### Testing +You can run integration and unit tests in interactive mode of IDE or in terminal mode using the command: +```bash +pytest tests/integration_tests +pytest tests/unit_tests +``` + +#### E2E tests have 2 options for launching (legacy runner): +- using a compiler based on the substrait code +- using an already built docker image (docker runner) + +#### Local environment variables: +- `LOCALNET_SH_PATH` - path to `localnet.sh` script in cloned subtensor repository (for legacy runner); +- `BUILD_BINARY` - (`=0` or `=1`) - used with `LOCALNET_SH_PATH` for build or not before start localnet node (for legacy runner); +- `USE_DOCKER` - (`=0` or `=1`) - used if you want to use specific runner to run e2e tests (for docker runner); +- `FAST_BLOCKS` - (`=0` or `=1`) - allows you to run a localnet node in fast or non-fast blocks mode (for both types of runers). + +#### Using `docker runner` (default for now): +- E2E tests with docker image do not require preliminary compilation +- are executed very quickly +- require docker installed in OS + +How to use: +```bash +pytest tests/e2e_tests +``` + +#### Using `legacy runner`: +- Will start compilation of the collected code in your subtensor repository +- you must provide the `LOCALNET_SH_PATH` variable in the local environment with the path to the file `/scripts/localnet.sh` in the cloned repository within your OS +- you can use the `BUILD_BINARY=0` variable, this will skip the copy step for each test. +- you can use the `USE_DOCKER=0` variable, this will run tests using the "legacy runner", even if docker is installed in your OS + +#### How to use: +Regular e2e tests run +```bash +LOCALNET_SH_PATH=/path/to/your/localnet.sh pytest tests/e2e_tests +``` + +If you want to skip re-build process for each e2e test +```bash +BUILD_BINARY=0 LOCALNET_SH_PATH=/path/to/your/localnet.sh pytest tests/e2e_tests +``` + +If you want to use legacy runner even with installed Docker in your OS +```bash +USE_DOCKER=0 BUILD_BINARY=0 LOCALNET_SH_PATH=/path/to/your/localnet.sh pytest tests/e2e_tests +``` + +--- + +## Release Guidelines +Instructions for the release manager: [RELEASE_GUIDELINES.md](./contrib/RELEASE_GUIDELINES.md) document. + +## Contributions +Ready to contribute? Read the [contributing guide](./contrib/CONTRIBUTING.md) before making a pull request. ## License The MIT License (MIT) -Copyright © 2020 +Copyright © 2024 The Opentensor Foundation Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: @@ -25,3 +305,6 @@ The above copyright notice and this permission notice shall be included in all c THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +## Acknowledgments +**learning-at-home/hivemind** diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000000..ef06884bb2 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,42 @@ +# Security Policy + +## Reporting a Vulnerability + +If you discover a security vulnerability in the Bittensor protocol, SDK, or any of its components, we strongly encourage you to report it responsibly. + +Please **do not publicly disclose** the vulnerability until we have had a reasonable chance to address it. + +### 🔐 Confidential Reporting + +To report a vulnerability, you can use any of the following methods: + +- Create a [GitHub Issue](https://github.com/opentensor/bittensor/issues) using the `Security` label or title. + +- Contact us via our official Discord support thread: [#btcli-btsdk](https://discord.com/channels/1120750674595024897/1242999357436071956) + +### 🧾 What to Include + +When reporting a vulnerability, please provide as much detail as possible: + +- Affected component (e.g., `bittensor`, `bittensor-cli`, `bittensor-wallet`, etc.) +- Version or commit hash +- Description of the vulnerability +- Steps to reproduce (if possible) +- Impact assessment +- Any potential mitigations or recommendations + +--- + +## Response Process + +1. We will acknowledge your report within **48 hours**. +2. We will investigate and confirm the issue. +3. If confirmed, we will coordinate on a fix and set an embargo period if needed. +4. A fix will be developed, tested, and released as soon as possible. +5. You will be credited (if you wish) in the security section of our release notes. + +--- + +## Thank You + +We appreciate your efforts in keeping the Bittensor ecosystem secure and responsible. diff --git a/adam_config.yaml b/adam_config.yaml deleted file mode 100644 index b593dcea63..0000000000 --- a/adam_config.yaml +++ /dev/null @@ -1,52 +0,0 @@ -axon: - max_gradients: 100 - max_workers: 10 - port: 8091 - remote_ip: 99.238.136.56 -dendrite: - do_backoff: true - key_dim: 100 - max_backoff: 100 - pass_gradients: true - stale_emit_filter: 10000 - timeout: 0.5 - topk: 10 -metagraph: - chain_endpoint: feynman.kusanagi.bittensor.com:9944 - stale_emit_filter: 10000 -neuron: - accumulation_interval: 5 - apply_remote_gradients: true - batch_size_train: 4 - datapath: data - learning_rate: 0.00002 - log_interval: 10 - momentum: 0.98 - name: Adam - sync_interval: 200 - trial_id: '1608300539' - trial_path: data/adam/1608300539 - record_log: true -nucleus: - max_workers: 5 - queue_maxsize: 10 - queue_timeout: 5 -session: - coldkeyfile: ~/.bittensor/coldkey - hotkeyfile: ~/.bittensor/hotkey - keypair: '5DaQQ6umBFmZRmj5pDPK1oXz6NWWRGjngBJozLFQJSM3PAP8' -synapse: - activation_function: gelu_new - attn_pdrop: 0.1 - embd_pdrop: 0.1 - initializer_range: 0.02 - layer_norm_epsilon: 1.0e-05 - n_head: 16 - n_inner: null - n_layer: 12 - resid_pdrop: 0.1 - summary_activation: null - summary_first_dropout: 0.1 - summary_proj_to_labels: true - summary_type: cls_index - summary_use_proj: true \ No newline at end of file diff --git a/bittensor/__init__.py b/bittensor/__init__.py index 9021c8d5a1..14b3b447c4 100644 --- a/bittensor/__init__.py +++ b/bittensor/__init__.py @@ -1,40 +1,17 @@ -from loguru import logger -import os -import sys -from bittensor.subtensor.interface import Keypair -from transformers import GPT2Tokenizer - -sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) - -from bittensor.config import Config -from bittensor.session import Session - -__version__ = '0.0.0' -__blocktime__ = 6 # seconds -__network_dim__ = 512 -__tokenizer__ = GPT2Tokenizer.from_pretrained("gpt2", local_files_only=False) -__tokenizer__.pad_token = '[PAD]' -__tokenizer__.mask_token = -100 -__vocab_size__ = 204483 -__max_sequence_length__ = 1024 - -# Default logger -logger_config = { - "handlers": [{ - "sink": - sys.stdout, - "format": - "{level: <8}|{name}:{function}:{line} - {message}", - }] -} -logger.configure(**logger_config) - -# Initialize the global bittensor session object. -session = None -def init(config: Config): - global session - - session = Session(config) - return session - - +import warnings + +from .core.settings import __version__, version_split, DEFAULTS, DEFAULT_NETWORK +from .utils.btlogging import logging +from .utils.easy_imports import * +from .utils.runtime_browser import runtime_browser as runtime +from .utils.runtime_async_browser import runtime_async_browser as async_runtime + + +def __getattr__(name): + if name == "version_split": + warnings.warn( + "version_split is deprecated and will be removed in future versions. Use __version__ instead.", + DeprecationWarning, + ) + return version_split + raise AttributeError(f"module {__name__} has no attribute {name}") diff --git a/bittensor/__main__.py b/bittensor/__main__.py new file mode 100644 index 0000000000..f3cc1e6487 --- /dev/null +++ b/bittensor/__main__.py @@ -0,0 +1,23 @@ +import os +import subprocess +import sys + +from bittensor import __version__ +from bittensor.utils.version import check_latest_version_in_pypi + +if __name__ == "__main__": + if len(sys.argv) > 1 and sys.argv[1] == "certifi": + # Resolve the path to certifi.sh + certifi_script = os.path.join(os.path.dirname(__file__), "utils", "certifi.sh") + if not os.path.exists(certifi_script): + print(f"Error: certifi.sh not found at {certifi_script}") + sys.exit(1) + + # Ensure the script is executable + os.chmod(certifi_script, 0o755) + + # Run the script + subprocess.run([certifi_script], check=True) + else: + print(f"Installed Bittensor SDK version: {__version__}") + check_latest_version_in_pypi() diff --git a/bittensor/__pycache__/__init__.cpython-38.pyc b/bittensor/__pycache__/__init__.cpython-38.pyc deleted file mode 100644 index bcf6f8e8f6..0000000000 Binary files a/bittensor/__pycache__/__init__.cpython-38.pyc and /dev/null differ diff --git a/bittensor/__pycache__/axon.cpython-38.pyc b/bittensor/__pycache__/axon.cpython-38.pyc deleted file mode 100644 index 3c365afc44..0000000000 Binary files a/bittensor/__pycache__/axon.cpython-38.pyc and /dev/null differ diff --git a/bittensor/__pycache__/bittensor_pb2.cpython-38.pyc b/bittensor/__pycache__/bittensor_pb2.cpython-38.pyc deleted file mode 100644 index 77fd9f5ec9..0000000000 Binary files a/bittensor/__pycache__/bittensor_pb2.cpython-38.pyc and /dev/null differ diff --git a/bittensor/__pycache__/bittensor_pb2_grpc.cpython-38.pyc b/bittensor/__pycache__/bittensor_pb2_grpc.cpython-38.pyc deleted file mode 100644 index 910f8d89ee..0000000000 Binary files a/bittensor/__pycache__/bittensor_pb2_grpc.cpython-38.pyc and /dev/null differ diff --git a/bittensor/__pycache__/config.cpython-38.pyc b/bittensor/__pycache__/config.cpython-38.pyc deleted file mode 100644 index c73611c67d..0000000000 Binary files a/bittensor/__pycache__/config.cpython-38.pyc and /dev/null differ diff --git a/bittensor/__pycache__/dendrite.cpython-38.pyc b/bittensor/__pycache__/dendrite.cpython-38.pyc deleted file mode 100644 index 6bad76bec5..0000000000 Binary files a/bittensor/__pycache__/dendrite.cpython-38.pyc and /dev/null differ diff --git a/bittensor/__pycache__/metadata.cpython-38.pyc b/bittensor/__pycache__/metadata.cpython-38.pyc deleted file mode 100644 index a9294a334b..0000000000 Binary files a/bittensor/__pycache__/metadata.cpython-38.pyc and /dev/null differ diff --git a/bittensor/__pycache__/metagraph.cpython-38.pyc b/bittensor/__pycache__/metagraph.cpython-38.pyc deleted file mode 100644 index cfd8bc8d64..0000000000 Binary files a/bittensor/__pycache__/metagraph.cpython-38.pyc and /dev/null differ diff --git a/bittensor/__pycache__/neuron.cpython-38.pyc b/bittensor/__pycache__/neuron.cpython-38.pyc deleted file mode 100644 index c3e80c348b..0000000000 Binary files a/bittensor/__pycache__/neuron.cpython-38.pyc and /dev/null differ diff --git a/bittensor/__pycache__/nucleus.cpython-38.pyc b/bittensor/__pycache__/nucleus.cpython-38.pyc deleted file mode 100644 index 67adfd0416..0000000000 Binary files a/bittensor/__pycache__/nucleus.cpython-38.pyc and /dev/null differ diff --git a/bittensor/__pycache__/serializer.cpython-38.pyc b/bittensor/__pycache__/serializer.cpython-38.pyc deleted file mode 100644 index d21779b12a..0000000000 Binary files a/bittensor/__pycache__/serializer.cpython-38.pyc and /dev/null differ diff --git a/bittensor/__pycache__/session.cpython-38.pyc b/bittensor/__pycache__/session.cpython-38.pyc deleted file mode 100644 index f768faf640..0000000000 Binary files a/bittensor/__pycache__/session.cpython-38.pyc and /dev/null differ diff --git a/bittensor/__pycache__/synapse.cpython-38.pyc b/bittensor/__pycache__/synapse.cpython-38.pyc deleted file mode 100644 index 331a62783d..0000000000 Binary files a/bittensor/__pycache__/synapse.cpython-38.pyc and /dev/null differ diff --git a/bittensor/__pycache__/tb_logger.cpython-38.pyc b/bittensor/__pycache__/tb_logger.cpython-38.pyc deleted file mode 100644 index 31fd92eda9..0000000000 Binary files a/bittensor/__pycache__/tb_logger.cpython-38.pyc and /dev/null differ diff --git a/bittensor/axon.py b/bittensor/axon.py deleted file mode 100644 index 93d7fb3737..0000000000 --- a/bittensor/axon.py +++ /dev/null @@ -1,392 +0,0 @@ -import argparse -import grpc -import random -import requests -import sys -import threading -import torch -import queue -import validators - -from concurrent import futures -from munch import Munch -from loguru import logger -from types import SimpleNamespace -from typing import List - -import bittensor -from bittensor.nucleus import Nucleus -from bittensor.synapse import Synapse -from bittensor import bittensor_pb2 -from bittensor import bittensor_pb2_grpc as bittensor_grpc -from bittensor.serializer import PyTorchSerializer - -def obtain_ip(config: Munch) -> Munch: - if config.axon.remote_ip != None: - return config - try: - value = requests.get('https://api.ipify.org').text - except Exception as e: - logger.error("CONFIG: Could not retrieve public facing IP from IP API. Exception: ".format(e)) - raise SystemError('CONFIG: Could not retrieve public facing IP from IP API.') - if not validators.ipv4(value): - logger.error("CONFIG: Response from IP API is not a valid IP with ip {}", value) - raise SystemError('CONFIG: Response from IP API is not a valid IP with ip {}'.format(value)) - config.axon.remote_ip = value - logger.info('remote ip: {}', value) - return config - -class Axon(bittensor_grpc.BittensorServicer): - r""" - A bittensor Axon serves a grpc endpoint which provides access to a single bittensor.synapse.Synapse - It recieves Forward and Backward requests and process the corresponding Synapse.call_forward and Synapse.call_backward. - - """ - def __init__(self, config, nucleus): - r""" Initializes a new Axon endpoint with passed config and keypair. - Args: - config (:obj:`Munch`, `required`): - bittensor Munch config. - nucleus (:obj:`bittensor.nucleus.Nucleus`, `required`): - backend processing nucleus. - """ - self._config = config - self.__keypair = config.session.keypair - self._nucleus = nucleus - - # Init server objects. - self._server = grpc.server(futures.ThreadPoolExecutor(max_workers=self._config.axon.max_workers)) - bittensor_grpc.add_BittensorServicer_to_server(self, self._server) - self._server.add_insecure_port('[::]:' + str(self._config.axon.port)) - - # Local synapse to serve. - self.synapse = None - self.priority = {} - self._next_unknown_priority_increment = 0 - - # Gradient queue - self.gradients = queue.PriorityQueue(maxsize = self._config.axon.max_gradients) - - # Serving thread. - self._thread = None - - # Stats. - #TODO(\u290B,\u290A) - self.stats = SimpleNamespace( - forward_in_bytes_per_second = 0.0, - backward_in_bytes_per_second = 0.0, - forward_out_bytes_per_second = 0.0, - backward_out_bytes_per_second = 0.0, - ) - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - r""" Adds this axon's command line arguments to the passed parser. - Args: - parser (:obj:`argparse.ArgumentParser`, `required`): - parser argument to append args to. - """ - parser.add_argument('--axon.port', default=8091, type=int, help='Port to serve axon') - parser.add_argument('--axon.remote_ip', default=None, type=str, help='Remote IP to serve to chain.') - parser.add_argument('--axon.max_workers', default=10, type=int, help='Max number connection handler threads working simultaneously.') - parser.add_argument('--axon.max_gradients', default=100, type=int, help='Max number of lingering gradient stored in the gradient queue') - - @staticmethod - def check_config(config: Munch): - r""" Checks the passed config items for validity and obtains the remote ip. - Args: - config (:obj:`munch.Munch, `required`): - config to check. - """ - logger.info('obtaining remote ip ...') - config = obtain_ip(config) - assert config.axon.port > 1024 and config.axon.port < 65535, 'config.axon.port must be in range [1024, 65535]' - - def serve(self, synapse: Synapse): - r""" Set the synapse being served on this axon endpoint. - This object's call_forward and call_backward will be - called on incoming Forward and Backward requests respectively. - - Args: - synapse (:obj:`bittensor.synapse.Synapse`, `required`): - synpase object to serve. - """ - self.synapse = synapse - - def set_priority(self, neurons: List[bittensor_pb2.Neuron], priority: torch.FloatTensor): - r""" Set the serving priority for requests on the served synapse. - Float values must are normalized to 1. - - Args: - neurons (:obj:`List[bittensor_pb2.Neuron]` of shape :obj:`(num_neurons)`, `required`): - List of remote neurons which match length of x. Tensors from x are sent forward to these neurons. - - priority (:obj:`torch.FloatTnsor` of shape :obj:`(num_neurons)`, `required`): - call priority for neurons on endpoint. - """ - assert priority.shape[0] == len(neurons), 'priority for neurons must of the same length' - if torch.sum(priority) != 0 and torch.sum(priority) != 0: - priority = priority / torch.sum(priority) - priority_map = {} - for neuron, priority in list(zip(neurons, priority.tolist())): - priority_map[neuron.public_key] = priority - self.priority = priority_map - - def Forward(self, request: bittensor_pb2.TensorMessage, context: grpc.ServicerContext) -> bittensor_pb2.TensorMessage: - r""" The function called by remote GRPC Forward requests from other neurons. - Forward is equivalent to a 'forward' pass through a neural network. - After checking request validity, passes the request to the nucleus for processing. - See bittensor_pb2.ReturnCode for all possible return codes. - Args: - request (:obj:`bittensor_pb2`, `required`): - Tensor request proto. - context (:obj:`grpc.ServicerContext`, `required`): - grpc server context. - Returns: - response: (bittensor_pb2.TensorMessage): - proto response carring the synapse forward output or None under failure. - """ - tensor, message, code = self._forward(request) - response = bittensor_pb2.TensorMessage( - version = bittensor.__version__, - public_key = self.__keypair.public_key, - return_code = code, - message = message, - tensors = [tensor] if tensor is not None else [], - ) - return response - - def Backward(self, request: bittensor_pb2.TensorMessage, context: grpc.ServicerContext) -> bittensor_pb2.TensorMessage: - r""" The function called by remote GRPC Backward requests from other neurons. - Backward is equivalent to a 'backward' gradient descent pass through a neural network. - After checking request validity, passes the request to the nucleus for processing. - See bittensor_pb2.ReturnCode for all possible return codes. - Args: - request (:obj:`bittensor_pb2`, `required`): - Tensor request proto. - context (:obj:`grpc.ServicerContext`, `required`): - grpc server context. - Returns: - response: (bittensor_pb2.TensorMessage): - proto response carring the synapse backward output or None under failure. - """ - tensor, message, code = self._backward(request) - response = bittensor_pb2.TensorMessage( - version = bittensor.__version__, - public_key = self.__keypair.public_key, - return_code = code, - message = message, - tensors = [tensor] if tensor is not None else [], - ) - return response - - def _forward(self, request): - r""" Performs validity checks on the grpc request before calling nucleus forward. - Returns a the output, message and code from the backend forward call. - Args: - request (:obj:`bittensor_pb2`, `required`): - Tensor request proto. - Returns: - response: (:obj:`bittensor_pb2.Tensor, `required`): - serialized tensor response from the nucleus call or None. - message: (str, `required`): - message associated with forward call, potentially error, or 'success'. - code: (:obj:`bittensor_pb2.ReturnCode, `required`) - return code associated with forward call i.e. Success of Timeout. - """ - - # ---- Check synapse exists ---- - if self.synapse == None: - message = "Remote axon not serving a synapse" - code = bittensor_pb2.ReturnCode.NotServingSynapse - return None, message, code - - # C---- heck Empty request ---- - if len(request.tensors) == 0: - message = "Forward request contains {} tensors, expected 1 tensor in the forward call".format(len(request.tensors)) - code = bittensor_pb2.ReturnCode.EmptyRequest - return None, message, code - - # ---- Check deserialization ---- - inputs = request.tensors[0] - try: - x = PyTorchSerializer.deserialize(inputs) - except Exception as e: - message = "Forward request deserialization failed with error {}".format(e) - code = bittensor_pb2.ReturnCode.RequestDeserializationException - return None, message, code - - - # ---- Check shape and modality ---- - if x.shape[0] < 1: - message = "Froward request batch dim exception with batch_size = {} ".format(x.shape[0]) - code = bittensor_pb2.ReturnCode.RequestShapeException - return None, message, code - - if x.shape[1] < 1: - message = "Forward request sequence dim exception with sequence_dim = {} ".format(x.shape[1]) - code = bittensor_pb2.ReturnCode.RequestShapeException - return None, message, code - - if inputs.modality == bittensor_pb2.Modality.TEXT: - if len(x.shape) != 2: - message = "Forward text input shape exception with len(request.shape) = {} must have rank 2.".format(len(x.shape)) - code = bittensor_pb2.ReturnCode.RequestShapeException - return None, message, code - - if inputs.modality == bittensor_pb2.Modality.IMAGE: - if len(x.shape) != 5: - message = "Forward image input shape exception for len(shape) = {} must have rank 5".format(len(x.shape)) - code = bittensor_pb2.ReturnCode.RequestShapeException - return None, message, code - - if inputs.modality == bittensor_pb2.Modality.TENSOR: - if len(x.shape) != 3: - message = "Forward message tensor input shape exception len(shape) = {} must have rank 3".format(len(x.shape)) - code = bittensor_pb2.ReturnCode.RequestShapeException - return None, message, code - - # --- Get call priority ---- - try: - call_priority = self.priority[request.public_key] + random.random() - except: - call_priority = 1 + random.random() - - # ---- Make Nucleus forward call. ---- - try: - outputs, message, code = self._nucleus.forward( - synapse = self.synapse.to(self.synapse.device), - inputs = x.to(self.synapse.device), - mode = inputs.modality, - priority = call_priority - ) - - # ---- Catch Nucleus errors ---- - if code != bittensor_pb2.ReturnCode.Success: - return None, message, code - - except Exception as e: - message = "Unknown exception when calling nucleus forward {}".format(e) - code = bittensor_pb2.ReturnCode.UnknownException - return None, message, code - - # ---- Serialize response ---- - try: - outputs_serialized = PyTorchSerializer.serialize_tensor(outputs) - - except Exception as e: - message = "Serializtion of forward response failed with error {} and inputs: {}".format(e, outputs) - code = bittensor_pb2.ReturnCode.ResponseDeserializationException - return None, message, code - - # ---- Return successful response ---- - return outputs_serialized, message, code - - - def _backward(self, request): - r""" Performs validity checks on the grpc request before calling nucleus backward. - Returns a the output, message and code from the backend backward call. - Args: - request (:obj:`bittensor_pb2`, `required`): - Tensor request proto. - Returns: - response: (:obj:`bittensor_pb2.Tensor, `required`): - serialized tensor response from the nucleus call or None. - message: (str, `required`): - message associated with forward call, potentially error, or 'success'. - code: (:obj:`bittensor_pb2.ReturnCode, `required`) - return code associated with forward call i.e. Success of Timeout. - """ - # ---- Check that we have a synapse ----. - if self.synapse == None: - message = "Remote axon not serving a synapse" - code = bittensor_pb2.ReturnCode.NotServingSynapse - return None, message, code - - # ---- Check request inputs ----. - if len(request.tensors) == 2: - inputs_x = request.tensors[0] - grads_dy = request.tensors[1] - modality_x = inputs_x.modality - else: - message = "During backward: There are {} tensors in the request, expected 2.".format(len(request.tensors)) - code = bittensor_pb2.ReturnCode.InvalidRequest - return None, message, code - - # ---- Deserialize request --- - try: - inputs_x = PyTorchSerializer.deserialize(inputs_x) - grads_dy = PyTorchSerializer.deserialize(grads_dy) - - except Exception as e: - message = "Backward request deserialization failed with unknown error {}".format(e) - code = bittensor_pb2.ReturnCode.RequestDeserializationException - return None, message, code - - # --- Get call priority ---- - try: - call_priority = self.priority[request.public_key] + random.random() - except: - call_priority = 1 + random.random() - - # ---- Save gradients to buffer for later use. --- - try: - self.gradients.put( (call_priority, (request.public_key, inputs_x, grads_dy, modality_x)) , block=False) - except queue.Full: - logger.trace('gradient queue is full at size: {}', self.gradients.qsize()) - - # ---- Nucleus backward call ---- - try: - outputs, message, code = self._nucleus.backward( - synapse = self.synapse, - inputs_x = inputs_x, - grads_dy = grads_dy, - modality = modality_x, - priority = call_priority - ) - except Exception as e: - message = "Unkown exception when calling backward with error {}".format(e) - code = bittensor_pb2.ReturnCode.UnknownException - return None, message, code - - # ---- Deserialize response ---- - try: - outputs_serialized = PyTorchSerializer.serialize_tensor(outputs) - except Exception as e: - message = "Backward request serialization failed with error {} and inputs {}".format(e, outputs_serialized) - code = bittensor_pb2.ReturnCode.ResponseSerializationException - return None, message, code - - # ---- Finaly return ---- - return outputs_serialized, message, code - - - def __del__(self): - r""" Called when this axon is deleted, ensures background threads shut down properly. - """ - self.stop() - - def start(self): - r""" Starts the standalone axon GRPC server thread. - """ - self._thread = threading.Thread(target=self._serve, daemon=False) - self._thread.start() - - def _serve(self): - try: - self._server.start() - except (KeyboardInterrupt, SystemExit): - self.stop() - except Exception as e: - logger.error(e) - - def stop(self): - r""" Stop the axon grpc server. - """ - logger.info('Shutting down the Nucleus...') - self._nucleus.stop() - if self._server != None: - self._server.stop(0) - - - diff --git a/bittensor/balance/__init__.py b/bittensor/balance/__init__.py deleted file mode 100644 index 61e60d6407..0000000000 --- a/bittensor/balance/__init__.py +++ /dev/null @@ -1,44 +0,0 @@ -class Balance: - unit = "Tao" - rao : int - tao: float - - # Balance should always be an int and represent the satoshi of our token:rao - def __init__(self, balance): - self.rao = balance - self.tao = self.rao / pow(10, 9) - - def __int__(self): - return self.rao - - def __float__(self): - return self.tao - - def __str__(self): - return "{unit:s} {balance:.9f}".format(unit=self.unit, balance=self.tao) - - - def __eq__(self, other): - return self.rao == other.rao - - def __ne__(self, other): - return self.rao != other.rao - - def __gt__(self, other): - return self.rao > other.rao - - def __lt__(self, other): - return self.rao < other.rao - - def __le__(self, other): - return self.rao <= other.rao - - def __ge__(self, other): - return self.rao >= other.rao - - @staticmethod - def from_float(amount : float): - rao = int(amount * pow(10, 9)) - return Balance(rao) - - diff --git a/bittensor/balance/__pycache__/__init__.cpython-38.pyc b/bittensor/balance/__pycache__/__init__.cpython-38.pyc deleted file mode 100644 index 8eea789b59..0000000000 Binary files a/bittensor/balance/__pycache__/__init__.cpython-38.pyc and /dev/null differ diff --git a/bittensor/bittensor.proto b/bittensor/bittensor.proto deleted file mode 100644 index 344b60cce9..0000000000 --- a/bittensor/bittensor.proto +++ /dev/null @@ -1,183 +0,0 @@ -syntax = "proto3"; - -// Service definition for tensor processing servers. -service Bittensor { - // Forward tensor request. - rpc Forward (TensorMessage) returns (TensorMessage) {} - - // Backward tensor request i.e. gradient. - rpc Backward (TensorMessage) returns (TensorMessage) {} -} - -// Neuron endpoint definition. -// Fully describes a tensor processing service for a bittensor Neuron. -// SIZE: (256 * 4) + (512 * 2) + (128 * 3) + 32 + 64 = 2504-bits (~314 bytes) -// NOTE: only the (address, port, identity) need to be stored in RAM. -// (address, port, identity) = (128 + 32 + 256) = 412 (52 bytes) -// Holding 20,000,000 endpoints in 1-GB of RAM and 6-GB of Disk. -message Neuron { - // Version: [REQUIRED] Strictly increasing protocol version identifier. - // Indentifies protocol version for backward compatibility. - // i.e. 0.0.0 - // SIZE: 24 bits (3-bytes) - string version = 1; - - // Neuron key: [REQUIRED] Ed25519 raw hex encoded public key. - // i.e. b'4c598ff31b68eb6c458c2dc51b25367fa213c566088077f46d93156148429d78' - // SIZE: 256-bits (32-bytes) - string public_key = 2; - - // Address: [REQUIRED] Neuron ip address. - // i.e. '0.0.0.0' or [2001:0db8:85a3:0000:0000:8a2e:0370:7334] - // SIZE: < 128-bits (16-bytes) - string address = 3; - - // Port: [REQUIRED] Neuron endpoint listening port. - // i.e. '8081' - // SIZE: 32-bits (4-bytes) - int32 port = 4; - - // IPType: [REQUIRED] Ip endpoint type, i.e. v6, or v4 - // i.e. '8081' - // SIZE: 32-bits (4-bytes) - int32 ip_type = 5; - - // uid: [REQUIRED] unique network identifier. - // i.e. '1223213221' - // SIZE: 64-bits (8-bytes) - int64 uid = 6; -} - -// TensorMessage -// Contains a payload of 1 or more serialized tensors and their definitions. -// Also contains information to identity and verify the sender. -// Protocol for bittensor-hivemind message passing. -// SIZE: 136 bytes + (tensor_size) -message TensorMessage { - // Version: [REQUIRED] Strictly increasing protocol version identifier. - // Indentifies protocol version for backward compatibility. - // i.e. '0.0.0' - // SIZE: 32 bits (4-bytes) - string version = 1; - - // Neuron key: [REQUIRED] Ed25519 raw hex encoded public key. - // Public key of the caller. Used to make a call to the public key of the neuron. - // Links message to calling neuron-account. - // i.e. b'4c598ff31b68eb6c458c2dc51b25367fa213c566088077f46d93156148429d78' - // SIZE: 256-bits (32-bytes) - string public_key = 2; - - // Nounce: [OPTIONAL] Incrementing nounce to identify message ordering. - // Used ensure with signature to protect against spoofing attacks. - // i.e. nounce = nounce + 1 - // SIZE: 32-bits (4-bytes) - int64 nounce = 3; - - // Signature: [OPTIONAL] Digital signature linking the nounce neuron_key. - // Prevents spoofing attacks where an adversary sends messages as other peers. - // NOTE: this field does not link the signature to the tensor inputs. New signatures do not need - // to be generated for each message and can be periodically created with a new incrementing nounce. - // Endpoints ensure that that signature is correct and that nounce is incrementing. - // $ python - // >>> from cryptography.hazmat.backends import default_backend - // >>> from cryptography.hazmat.primitives import hashes - // >>> digest = hashes.Hash(hashes.SHA1(), backend=default_backend()) - // >>> digest.update(public_key.encode('utf-8')) - // >>> digest.update(bytes(nounce)) - // >>> digest = digest.finalize() - // signature = private_key.sign(digest) # to create. - // >>> assert (nounce >= last_nounce) and neuron_key.verify(signature, digest) # to verify. - bytes signature = 4; - - // Return codes from Backward and Forward call. - ReturnCode return_code = 5; - - // Message associated with return code. - string message = 6; - - // Tensors: [REQUIRED] 1 or more tensors passed on the wire. - // SIZE: variable. - // NOTE: During backward calls the tensors should be ordered [input_1, input_2, ... grad_1, grad_2, ...] - repeated Tensor tensors = 7; -} - -// Return codes from Backward and Forward call. -enum ReturnCode { - Success = 0; // Succesfull query. - Timeout = 1; // Request timeout. - Backoff = 2; - Unavailable = 3; - NotImplemented = 4; - EmptyRequest = 5; // Request is empty. - EmptyResponse = 6; // Response is empty. - InvalidResponse = 7; // Request is invalid. - InvalidRequest = 8; // Response is invalid. - RequestShapeException = 9; // Request has invalid shape. - ResponseShapeException = 10; // Response has invalid shape. - RequestSerializationException = 11; // Request failed to serialize. - ResponseSerializationException = 12; // Response failed to serialize. - RequestDeserializationException = 13; // Request failed to deserialize. - ResponseDeserializationException = 14; // Response failed to deserialize. - NotServingSynapse = 15; // Axon is not serving a synapse to query. - NucleusTimeout = 16; // Processing on the server side timedout. - NucleusFull = 17; // Returned when the processing queue on the server is full. - UnknownException = 18; // Unknown exception. -} - -// A serialized tensor object created using the serializer class. -// SIZE: 32 bytes + variable buffer size. -message Tensor { - // Version: [REQUIRED] Strictly increasing protocol version identifier. - // Indentifies protocol version for backward compatibility. - // i.e. '0.0.0' - // SIZE: 32 bits (4-bytes) - string version = 1; - - // Buffer: [REQUIRED] Serialized raw tensor content. This representation - // can be used for all tensor types. The purpose of this representation is to - // reduce serialization overhead during RPC calls by avoiding serialization of - // many repeated small items. In other words, we just copy the bytes and let - // the application layer serialize and deserialize the bytes. - // e.g. - // 1. PyTorchSerialzier.serialize(torch.Tensor) --> bittensor_pb2.Tensor - // 2. PyTorchSerializer.deserialize(bittensor_pb2.Tensor) --> torch.Tensor - // SIZE: variable - bytes buffer = 2; - - // Shape: [REQUIRED] Shape of this tensor. - // NOTE: Variable dimensions (i.e. batch) are non-explicit here as -1. - // ~ 5 * int32 = 128 bits - (16 bytes) - repeated int64 shape = 3; - - // Dtype: [REQUIRED] The tensor datatype. - // Used for serialization deserialization. - // int32 32-bits (4-bytes) - DataType dtype = 4; - - // Modality: TEXT, TENSOR, IMAGE - Modality modality = 5; - - // Requires grad: [OPTIONAL] Does this tensor require a gradient. - // 1 bit. - bool requires_grad = 6; -} - - -// Dtype: [REQUIRED] The tensor datatype. -// Used for serialization deserialization. -// int32 32-bits (4-bytes) -enum DataType { - UNKNOWN = 0; - FLOAT32 = 1; - FLOAT64 = 2; - INT32 = 3; - INT64 = 4; - UTF8 = 5; -} - -enum Modality { - TENSOR = 0; - IMAGE = 1; - TEXT = 2; -} - diff --git a/bittensor/bittensor_pb2.py b/bittensor/bittensor_pb2.py deleted file mode 100644 index 08ef56184d..0000000000 --- a/bittensor/bittensor_pb2.py +++ /dev/null @@ -1,522 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# source: bittensor/bittensor.proto -"""Generated protocol buffer code.""" -from google.protobuf.internal import enum_type_wrapper -from google.protobuf import descriptor as _descriptor -from google.protobuf import message as _message -from google.protobuf import reflection as _reflection -from google.protobuf import symbol_database as _symbol_database -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - - - -DESCRIPTOR = _descriptor.FileDescriptor( - name='bittensor/bittensor.proto', - package='', - syntax='proto3', - serialized_options=None, - create_key=_descriptor._internal_create_key, - serialized_pb=b'\n\x19\x62ittensor/bittensor.proto\"j\n\x06Neuron\x12\x0f\n\x07version\x18\x01 \x01(\t\x12\x12\n\npublic_key\x18\x02 \x01(\t\x12\x0f\n\x07\x61\x64\x64ress\x18\x03 \x01(\t\x12\x0c\n\x04port\x18\x04 \x01(\x05\x12\x0f\n\x07ip_type\x18\x05 \x01(\x05\x12\x0b\n\x03uid\x18\x06 \x01(\x03\"\xa4\x01\n\rTensorMessage\x12\x0f\n\x07version\x18\x01 \x01(\t\x12\x12\n\npublic_key\x18\x02 \x01(\t\x12\x0e\n\x06nounce\x18\x03 \x01(\x03\x12\x11\n\tsignature\x18\x04 \x01(\x0c\x12 \n\x0breturn_code\x18\x05 \x01(\x0e\x32\x0b.ReturnCode\x12\x0f\n\x07message\x18\x06 \x01(\t\x12\x18\n\x07tensors\x18\x07 \x03(\x0b\x32\x07.Tensor\"\x86\x01\n\x06Tensor\x12\x0f\n\x07version\x18\x01 \x01(\t\x12\x0e\n\x06\x62uffer\x18\x02 \x01(\x0c\x12\r\n\x05shape\x18\x03 \x03(\x03\x12\x18\n\x05\x64type\x18\x04 \x01(\x0e\x32\t.DataType\x12\x1b\n\x08modality\x18\x05 \x01(\x0e\x32\t.Modality\x12\x15\n\rrequires_grad\x18\x06 \x01(\x08*\xc1\x03\n\nReturnCode\x12\x0b\n\x07Success\x10\x00\x12\x0b\n\x07Timeout\x10\x01\x12\x0b\n\x07\x42\x61\x63koff\x10\x02\x12\x0f\n\x0bUnavailable\x10\x03\x12\x12\n\x0eNotImplemented\x10\x04\x12\x10\n\x0c\x45mptyRequest\x10\x05\x12\x11\n\rEmptyResponse\x10\x06\x12\x13\n\x0fInvalidResponse\x10\x07\x12\x12\n\x0eInvalidRequest\x10\x08\x12\x19\n\x15RequestShapeException\x10\t\x12\x1a\n\x16ResponseShapeException\x10\n\x12!\n\x1dRequestSerializationException\x10\x0b\x12\"\n\x1eResponseSerializationException\x10\x0c\x12#\n\x1fRequestDeserializationException\x10\r\x12$\n ResponseDeserializationException\x10\x0e\x12\x15\n\x11NotServingSynapse\x10\x0f\x12\x12\n\x0eNucleusTimeout\x10\x10\x12\x0f\n\x0bNucleusFull\x10\x11\x12\x14\n\x10UnknownException\x10\x12*Q\n\x08\x44\x61taType\x12\x0b\n\x07UNKNOWN\x10\x00\x12\x0b\n\x07\x46LOAT32\x10\x01\x12\x0b\n\x07\x46LOAT64\x10\x02\x12\t\n\x05INT32\x10\x03\x12\t\n\x05INT64\x10\x04\x12\x08\n\x04UTF8\x10\x05*+\n\x08Modality\x12\n\n\x06TENSOR\x10\x00\x12\t\n\x05IMAGE\x10\x01\x12\x08\n\x04TEXT\x10\x02\x32\x66\n\tBittensor\x12+\n\x07\x46orward\x12\x0e.TensorMessage\x1a\x0e.TensorMessage\"\x00\x12,\n\x08\x42\x61\x63kward\x12\x0e.TensorMessage\x1a\x0e.TensorMessage\"\x00\x62\x06proto3' -) - -_RETURNCODE = _descriptor.EnumDescriptor( - name='ReturnCode', - full_name='ReturnCode', - filename=None, - file=DESCRIPTOR, - create_key=_descriptor._internal_create_key, - values=[ - _descriptor.EnumValueDescriptor( - name='Success', index=0, number=0, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='Timeout', index=1, number=1, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='Backoff', index=2, number=2, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='Unavailable', index=3, number=3, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='NotImplemented', index=4, number=4, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='EmptyRequest', index=5, number=5, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='EmptyResponse', index=6, number=6, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='InvalidResponse', index=7, number=7, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='InvalidRequest', index=8, number=8, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='RequestShapeException', index=9, number=9, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='ResponseShapeException', index=10, number=10, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='RequestSerializationException', index=11, number=11, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='ResponseSerializationException', index=12, number=12, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='RequestDeserializationException', index=13, number=13, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='ResponseDeserializationException', index=14, number=14, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='NotServingSynapse', index=15, number=15, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='NucleusTimeout', index=16, number=16, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='NucleusFull', index=17, number=17, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='UnknownException', index=18, number=18, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - ], - containing_type=None, - serialized_options=None, - serialized_start=442, - serialized_end=891, -) -_sym_db.RegisterEnumDescriptor(_RETURNCODE) - -ReturnCode = enum_type_wrapper.EnumTypeWrapper(_RETURNCODE) -_DATATYPE = _descriptor.EnumDescriptor( - name='DataType', - full_name='DataType', - filename=None, - file=DESCRIPTOR, - create_key=_descriptor._internal_create_key, - values=[ - _descriptor.EnumValueDescriptor( - name='UNKNOWN', index=0, number=0, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='FLOAT32', index=1, number=1, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='FLOAT64', index=2, number=2, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='INT32', index=3, number=3, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='INT64', index=4, number=4, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='UTF8', index=5, number=5, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - ], - containing_type=None, - serialized_options=None, - serialized_start=893, - serialized_end=974, -) -_sym_db.RegisterEnumDescriptor(_DATATYPE) - -DataType = enum_type_wrapper.EnumTypeWrapper(_DATATYPE) -_MODALITY = _descriptor.EnumDescriptor( - name='Modality', - full_name='Modality', - filename=None, - file=DESCRIPTOR, - create_key=_descriptor._internal_create_key, - values=[ - _descriptor.EnumValueDescriptor( - name='TENSOR', index=0, number=0, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='IMAGE', index=1, number=1, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - _descriptor.EnumValueDescriptor( - name='TEXT', index=2, number=2, - serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), - ], - containing_type=None, - serialized_options=None, - serialized_start=976, - serialized_end=1019, -) -_sym_db.RegisterEnumDescriptor(_MODALITY) - -Modality = enum_type_wrapper.EnumTypeWrapper(_MODALITY) -Success = 0 -Timeout = 1 -Backoff = 2 -Unavailable = 3 -NotImplemented = 4 -EmptyRequest = 5 -EmptyResponse = 6 -InvalidResponse = 7 -InvalidRequest = 8 -RequestShapeException = 9 -ResponseShapeException = 10 -RequestSerializationException = 11 -ResponseSerializationException = 12 -RequestDeserializationException = 13 -ResponseDeserializationException = 14 -NotServingSynapse = 15 -NucleusTimeout = 16 -NucleusFull = 17 -UnknownException = 18 -UNKNOWN = 0 -FLOAT32 = 1 -FLOAT64 = 2 -INT32 = 3 -INT64 = 4 -UTF8 = 5 -TENSOR = 0 -IMAGE = 1 -TEXT = 2 - - - -_NEURON = _descriptor.Descriptor( - name='Neuron', - full_name='Neuron', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='version', full_name='Neuron.version', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='public_key', full_name='Neuron.public_key', index=1, - number=2, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='address', full_name='Neuron.address', index=2, - number=3, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='port', full_name='Neuron.port', index=3, - number=4, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='ip_type', full_name='Neuron.ip_type', index=4, - number=5, type=5, cpp_type=1, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='uid', full_name='Neuron.uid', index=5, - number=6, type=3, cpp_type=2, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=29, - serialized_end=135, -) - - -_TENSORMESSAGE = _descriptor.Descriptor( - name='TensorMessage', - full_name='TensorMessage', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='version', full_name='TensorMessage.version', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='public_key', full_name='TensorMessage.public_key', index=1, - number=2, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='nounce', full_name='TensorMessage.nounce', index=2, - number=3, type=3, cpp_type=2, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='signature', full_name='TensorMessage.signature', index=3, - number=4, type=12, cpp_type=9, label=1, - has_default_value=False, default_value=b"", - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='return_code', full_name='TensorMessage.return_code', index=4, - number=5, type=14, cpp_type=8, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='message', full_name='TensorMessage.message', index=5, - number=6, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='tensors', full_name='TensorMessage.tensors', index=6, - number=7, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=138, - serialized_end=302, -) - - -_TENSOR = _descriptor.Descriptor( - name='Tensor', - full_name='Tensor', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='version', full_name='Tensor.version', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='buffer', full_name='Tensor.buffer', index=1, - number=2, type=12, cpp_type=9, label=1, - has_default_value=False, default_value=b"", - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='shape', full_name='Tensor.shape', index=2, - number=3, type=3, cpp_type=2, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='dtype', full_name='Tensor.dtype', index=3, - number=4, type=14, cpp_type=8, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='modality', full_name='Tensor.modality', index=4, - number=5, type=14, cpp_type=8, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='requires_grad', full_name='Tensor.requires_grad', index=5, - number=6, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=305, - serialized_end=439, -) - -_TENSORMESSAGE.fields_by_name['return_code'].enum_type = _RETURNCODE -_TENSORMESSAGE.fields_by_name['tensors'].message_type = _TENSOR -_TENSOR.fields_by_name['dtype'].enum_type = _DATATYPE -_TENSOR.fields_by_name['modality'].enum_type = _MODALITY -DESCRIPTOR.message_types_by_name['Neuron'] = _NEURON -DESCRIPTOR.message_types_by_name['TensorMessage'] = _TENSORMESSAGE -DESCRIPTOR.message_types_by_name['Tensor'] = _TENSOR -DESCRIPTOR.enum_types_by_name['ReturnCode'] = _RETURNCODE -DESCRIPTOR.enum_types_by_name['DataType'] = _DATATYPE -DESCRIPTOR.enum_types_by_name['Modality'] = _MODALITY -_sym_db.RegisterFileDescriptor(DESCRIPTOR) - -Neuron = _reflection.GeneratedProtocolMessageType('Neuron', (_message.Message,), { - 'DESCRIPTOR' : _NEURON, - '__module__' : 'bittensor.bittensor_pb2' - # @@protoc_insertion_point(class_scope:Neuron) - }) -_sym_db.RegisterMessage(Neuron) - -TensorMessage = _reflection.GeneratedProtocolMessageType('TensorMessage', (_message.Message,), { - 'DESCRIPTOR' : _TENSORMESSAGE, - '__module__' : 'bittensor.bittensor_pb2' - # @@protoc_insertion_point(class_scope:TensorMessage) - }) -_sym_db.RegisterMessage(TensorMessage) - -Tensor = _reflection.GeneratedProtocolMessageType('Tensor', (_message.Message,), { - 'DESCRIPTOR' : _TENSOR, - '__module__' : 'bittensor.bittensor_pb2' - # @@protoc_insertion_point(class_scope:Tensor) - }) -_sym_db.RegisterMessage(Tensor) - - - -_BITTENSOR = _descriptor.ServiceDescriptor( - name='Bittensor', - full_name='Bittensor', - file=DESCRIPTOR, - index=0, - serialized_options=None, - create_key=_descriptor._internal_create_key, - serialized_start=1021, - serialized_end=1123, - methods=[ - _descriptor.MethodDescriptor( - name='Forward', - full_name='Bittensor.Forward', - index=0, - containing_service=None, - input_type=_TENSORMESSAGE, - output_type=_TENSORMESSAGE, - serialized_options=None, - create_key=_descriptor._internal_create_key, - ), - _descriptor.MethodDescriptor( - name='Backward', - full_name='Bittensor.Backward', - index=1, - containing_service=None, - input_type=_TENSORMESSAGE, - output_type=_TENSORMESSAGE, - serialized_options=None, - create_key=_descriptor._internal_create_key, - ), -]) -_sym_db.RegisterServiceDescriptor(_BITTENSOR) - -DESCRIPTOR.services_by_name['Bittensor'] = _BITTENSOR - -# @@protoc_insertion_point(module_scope) diff --git a/bittensor/bittensor_pb2_grpc.py b/bittensor/bittensor_pb2_grpc.py deleted file mode 100644 index fdb47d3f84..0000000000 --- a/bittensor/bittensor_pb2_grpc.py +++ /dev/null @@ -1,104 +0,0 @@ -# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! -"""Client and server classes corresponding to protobuf-defined services.""" -import grpc - -from bittensor import bittensor_pb2 as bittensor_dot_bittensor__pb2 - - -class BittensorStub(object): - """Service definition for tensor processing servers. - """ - - def __init__(self, channel): - """Constructor. - - Args: - channel: A grpc.Channel. - """ - self.Forward = channel.unary_unary( - '/Bittensor/Forward', - request_serializer=bittensor_dot_bittensor__pb2.TensorMessage.SerializeToString, - response_deserializer=bittensor_dot_bittensor__pb2.TensorMessage.FromString, - ) - self.Backward = channel.unary_unary( - '/Bittensor/Backward', - request_serializer=bittensor_dot_bittensor__pb2.TensorMessage.SerializeToString, - response_deserializer=bittensor_dot_bittensor__pb2.TensorMessage.FromString, - ) - - -class BittensorServicer(object): - """Service definition for tensor processing servers. - """ - - def Forward(self, request, context): - """Forward tensor request. - """ - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def Backward(self, request, context): - """Backward tensor request i.e. gradient. - """ - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - -def add_BittensorServicer_to_server(servicer, server): - rpc_method_handlers = { - 'Forward': grpc.unary_unary_rpc_method_handler( - servicer.Forward, - request_deserializer=bittensor_dot_bittensor__pb2.TensorMessage.FromString, - response_serializer=bittensor_dot_bittensor__pb2.TensorMessage.SerializeToString, - ), - 'Backward': grpc.unary_unary_rpc_method_handler( - servicer.Backward, - request_deserializer=bittensor_dot_bittensor__pb2.TensorMessage.FromString, - response_serializer=bittensor_dot_bittensor__pb2.TensorMessage.SerializeToString, - ), - } - generic_handler = grpc.method_handlers_generic_handler( - 'Bittensor', rpc_method_handlers) - server.add_generic_rpc_handlers((generic_handler,)) - - - # This class is part of an EXPERIMENTAL API. -class Bittensor(object): - """Service definition for tensor processing servers. - """ - - @staticmethod - def Forward(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary(request, target, '/Bittensor/Forward', - bittensor_dot_bittensor__pb2.TensorMessage.SerializeToString, - bittensor_dot_bittensor__pb2.TensorMessage.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) - - @staticmethod - def Backward(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary(request, target, '/Bittensor/Backward', - bittensor_dot_bittensor__pb2.TensorMessage.SerializeToString, - bittensor_dot_bittensor__pb2.TensorMessage.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) diff --git a/bittensor/config.py b/bittensor/config.py deleted file mode 100644 index 5da81a6e0f..0000000000 --- a/bittensor/config.py +++ /dev/null @@ -1,359 +0,0 @@ -import requests -from loguru import logger -import argparse -from importlib.machinery import SourceFileLoader -import munch -import os -import pathlib -import validators -import yaml -import stat -from munch import Munch - -from bittensor.axon import Axon -from bittensor.session import Session -from bittensor.dendrite import Dendrite -from bittensor.metagraph import Metagraph -from bittensor.crypto import KeyError -from bittensor.crypto.keyfiles import KeyFileError -from bittensor.nucleus import Nucleus - - - -from bittensor.session import KeyFileError - -class InvalidConfigFile(Exception): - pass - -class ValidationError(Exception): - pass - -class PostProcessingError(Exception): - pass - -class InvalidConfigError(Exception): - pass - -class MustPassNeuronPath(Exception): - pass - -class Config: - @staticmethod - def toString(items) -> str: - - print(items.toDict()) - return "\n" + yaml.dump(items.toDict()) - - @staticmethod - def load(parser: argparse.ArgumentParser = None) -> Munch: - r""" Loads and return the bittensor Munched config. - - Args: - parser (str, `required`): - argument parser. - - Returns: - config (:obj:`Munch` `required`): - Python Munch object with values from parsed string. - """ - if parser == None: - parser = argparse.ArgumentParser() - - # 0. Check for and create .bittensor directory - Config.check_and_create_config_dir() - - - # 1. Load args from bittensor backend components. - Axon.add_args(parser) - Dendrite.add_args(parser) - Metagraph.add_args(parser) - Session.add_args(parser) - Nucleus.add_args(parser) - - # 2. Parse. - params = parser.parse_known_args()[0] - config_file = None - if 'neuron.config_file' in vars(params).keys(): - config_file = vars(params)['neuron.config_file'] - - if config_file: - config = Config.load_from_relative_path(config_file) - else: - # 3. Splits params on dot syntax i.e session.axon_port - config = Munch() - for arg_key, arg_val in params.__dict__.items(): - split_keys = arg_key.split('.') - if len(split_keys) == 1: - config[arg_key] = arg_val - else: - head = config - for key in split_keys[:-1]: - if key not in config: - head[key] = Munch() - head = head[key] - head[split_keys[-1]] = arg_val - - # 4. Run session checks. - try: - Dendrite.check_config(config) - Session.check_config(config) - Metagraph.check_config(config) - Axon.check_config(config) - Nucleus.check_config(config) - - except KeyFileError: - quit() - - - #5. Load key - try: - Session.load_hotkeypair(config) - Session.load_cold_key(config) - except (KeyError): - logger.error("Invalid password") - quit() - except KeyFileError: - logger.error("Keyfile corrupt") - quit() - - return config - - @staticmethod - def check_and_create_config_dir(): - path = "~/.bittensor" - path = os.path.expanduser(path) - - if not os.path.isdir(path): - Config.create_config_dir(path) - - - @staticmethod - def create_config_dir(path): - logger.info("Creating {} config dir", path) - os.makedirs(path, exist_ok=True) - os.chmod(path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR) - - - @staticmethod - def load_from_relative_path(path: str) -> Munch: - r""" Loads and returns a Munched config object from a relative path. - - Args: - path (str, `required`): - Path to config.yaml file. full_path = cwd() + path - - Returns: - config (:obj:`Munch` `required`): - Python Munch object with values from config under path. - """ - # Load yaml items from relative path. - path_items = munch.Munch() - if path != None: - path = os.getcwd() + '/' + path - if not os.path.isfile(path): - logger.error('CONFIG: cannot find passed configuration file at {}', path) - raise FileNotFoundError('Cannot find a configuration file at', path) - with open(path, 'r') as f: - try: - path_items = yaml.safe_load(f) - path_items = munch.munchify(path_items) - except yaml.YAMLError as exc: - logger.error('CONFIG: cannot parse passed configuration file at {}', path) - raise InvalidConfigFile - return path_items - - - @staticmethod - def load_from_yaml_string(yaml_str: str) -> Munch: - r""" Loads and returns a Munched config object from a passed string - - Args: - yaml_str (str, `required`): - String representation of yaml file. - - Returns: - config (:obj:`Munch` `required`): - Python Munch object with values from parsed string. - """ - # Load items yaml string. - yaml_items = munch.Munch() - if yaml_str != None: - try: - yaml_items = yaml.safe_load(yaml_str) - yaml_items = munch.munchify(yaml_items) - except Exception as e: - logger.error('CONFIG: failed parsing passed yaml with input {}. Exception: {}'.format(yaml_str, e)) - raise InvalidConfigFile - return yaml_items - - @staticmethod - def validate(config, neuron_path) -> Munch: - # 1. Run session checks. - config = Dendrite.check_config(config) - config = Session.check_config(config) - config = Metagraph.check_config(config) - config = Axon.check_config(config) - - # 2. Run neuron checks. - neuron_module = SourceFileLoader("Neuron", os.getcwd() + '/' + neuron_path + '/neuron.py').load_module() - config = neuron_module.Neuron.check_config( config ) - return config - - @staticmethod - def post_process(items): - try: - # 7.1 Optain remote ip. - Config.obtain_ip_address(items) - - # 7.2 Fix paths. - Config.fix_paths(items) - - except PostProcessingError: - logger.debug("CONFIG: post processing error.") - raise InvalidConfigError - - @staticmethod - def validate_socket(key, value: str): - def error(): - message = "CONFIG: Validation error: {} for option {} is not a valid socket definition : :" - logger.error(message, value, key) - raise ValidationError - - if ':' not in value: - error() - - elems = value.split(":") - if len(elems) != 2: - error() - - ip, port = elems - if not validators.ipv4(ip) and not validators.domain(ip) and ip != "localhost": - error() - - if not validators.between(int(port), min=1, max=65535): - error() - - @staticmethod - def validate_ip(key, value): - if not validators.ipv4(value): - logger.error("CONFIG: Validation error: {} for option {} is not a valid ip address", value, key) - raise ValidationError - - @staticmethod - def validate_int_range(key, value, min, max): - """ - Validates if a specifed integer falls in the specified range - """ - if not validators.between(value, min=min, max=max): - logger.error( - "CONFIG: Validation error: {} should be between {} and {}.", key, min, max) - raise ValidationError - - @staticmethod - def validate_path_create(key, value): - try: - full_neuron_path = os.getcwd() + '/' + value - pathlib.Path(full_neuron_path).mkdir(parents=True, exist_ok=True) - except PermissionError: - logger.error("CONFIG: Validation error: no permission to create path {} for option {}", value, key) - raise ValidationError - except Exception as e: - logger.error("CONFIG: Validation error: An undefined error occured while trying to create path {} for option {}. Exception: {}", value, key, e) - raise ValidationError - - @staticmethod - def obtain_ip_address(items): - if items.axon.remote_ip: - return - - try: - value = requests.get('https://api.ipify.org').text - except Exception as e: - logger.error("CONFIG: Could not retrieve public facing IP from IP API. Exception: {}", e) - raise PostProcessingError - - if not validators.ipv4(value): - logger.error("CONFIG: Response from IP API is not a valid IP.") - raise PostProcessingError - items.axon.remote_ip = value - return items - - @staticmethod - def overwrite_add(items_a, items_b): - r""" Overwrites and adds values from items A with items B. Returns - - Args: - items_a (obj:Munch, `required`): - Items to overrite into - - items_b (obj:Munch, `required`): - Items to overrite from - - Returns: - items (:obj:`Munch` `Optional`): - Overritten items or None if both are None. - """ - if items_b == None: - return items_a - if items_a == None: - return items_b - items_a = Config.overwrite(items_a, items_b) - items_a = Config.add(items_a, items_b) - return items_a - - @staticmethod - def overwrite(items_a, items_b): - r""" Overwrites values from items_b into items b. - - Args: - items_a (obj:Munch, `required`): - Items to overrite into - - items_b (obj:Munch, `required`): - Items to overrite from - - Returns: - items (:obj:`Munch` `Optional`): - Items with overwrite from B to A. - """ - for k, v in items_a.items(): - if k in items_b: - if isinstance(v, dict): - Config.overwrite(v, items_b[k]) - else: - if items_b != None: - items_a[k] = items_b[k] - return items_a - - @staticmethod - def add(items_a, items_b): - r""" Adds values from items b into items b. - - Args: - items_a (obj:Munch, `required`): - Items to overrite into - - items_b (obj:Munch, `required`): - Items to overrite from - - Returns: - items (:obj:`Munch` `Optional`): - Items with union from A and B. - """ - for k, v in items_b.items(): - if k not in items_a: - items_a[k] = items_b[k] - if k in items_a: - if isinstance(v, dict): - Config.add(items_a[k], items_b[k]) - return items_a - - @staticmethod - def update_replicate_yaml(neuron_name): - with open("replicate.yaml") as replicate_file: - replicate_yaml = yaml.safe_load(replicate_file) - - replicate_yaml['repository'] = "file://data/{}/.replicate".format(neuron_name) - - with open("replicate.yaml", "w") as f: - yaml.dump(replicate_yaml, f) \ No newline at end of file diff --git a/__init__.py b/bittensor/core/__init__.py similarity index 100% rename from __init__.py rename to bittensor/core/__init__.py diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py new file mode 100644 index 0000000000..14f91c1854 --- /dev/null +++ b/bittensor/core/async_subtensor.py @@ -0,0 +1,5836 @@ +import asyncio +import copy +import ssl +from datetime import datetime, timezone +from typing import cast, Optional, Any, Union, Iterable, TYPE_CHECKING + +import asyncstdlib as a +import numpy as np +import scalecodec +from async_substrate_interface import AsyncSubstrateInterface +from async_substrate_interface.substrate_addons import RetryAsyncSubstrate +from async_substrate_interface.utils.storage import StorageKey +from bittensor_drand import get_encrypted_commitment +from bittensor_wallet.utils import SS58_FORMAT +from numpy.typing import NDArray +from scalecodec import GenericCall + +from bittensor.core.chain_data import ( + DelegateInfo, + DynamicInfo, + MetagraphInfo, + NeuronInfoLite, + NeuronInfo, + ProposalVoteData, + SelectiveMetagraphIndex, + StakeInfo, + SubnetHyperparameters, + 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, +) +from bittensor.core.config import Config +from bittensor.core.errors import ChainError, SubstrateRequestException +from bittensor.core.extrinsics.asyncex.children import ( + root_set_pending_childkey_cooldown_extrinsic, + set_children_extrinsic, +) +from bittensor.core.extrinsics.asyncex.commit_reveal import commit_reveal_extrinsic +from bittensor.core.extrinsics.asyncex.liquidity import ( + add_liquidity_extrinsic, + modify_liquidity_extrinsic, + remove_liquidity_extrinsic, + toggle_user_liquidity_extrinsic, +) +from bittensor.core.extrinsics.asyncex.move_stake import ( + transfer_stake_extrinsic, + swap_stake_extrinsic, + move_stake_extrinsic, +) +from bittensor.core.extrinsics.asyncex.registration import ( + burned_register_extrinsic, + register_extrinsic, + register_subnet_extrinsic, + set_subnet_identity_extrinsic, +) +from bittensor.core.extrinsics.asyncex.root import root_register_extrinsic +from bittensor.core.extrinsics.asyncex.serving import ( + get_last_bonds_reset, + publish_metadata, + get_metadata, +) +from bittensor.core.extrinsics.asyncex.serving import serve_axon_extrinsic +from bittensor.core.extrinsics.asyncex.staking import ( + add_stake_extrinsic, + add_stake_multiple_extrinsic, +) +from bittensor.core.extrinsics.asyncex.start_call import start_call_extrinsic +from bittensor.core.extrinsics.asyncex.take import ( + decrease_take_extrinsic, + increase_take_extrinsic, +) +from bittensor.core.extrinsics.asyncex.transfer import transfer_extrinsic +from bittensor.core.extrinsics.asyncex.unstaking import ( + unstake_all_extrinsic, + unstake_extrinsic, + unstake_multiple_extrinsic, +) +from bittensor.core.extrinsics.asyncex.weights import ( + commit_weights_extrinsic, + set_weights_extrinsic, + reveal_weights_extrinsic, +) +from bittensor.core.metagraph import AsyncMetagraph +from bittensor.core.settings import version_as_int, TYPE_REGISTRY +from bittensor.core.types import ParamWithTypes, SubtensorMixin +from bittensor.utils import ( + Certificate, + decode_hex_identity_dict, + format_error_message, + is_valid_ss58_address, + torch, + u16_normalized_float, + u64_normalized_float, + get_transfer_fn_params, +) +from bittensor.utils import deprecated_message +from bittensor.utils.balance import ( + Balance, + fixed_to_float, + check_and_convert_to_balance, +) +from bittensor.utils.btlogging import logging +from bittensor.utils.liquidity import ( + calculate_fees, + get_fees, + tick_to_price, + price_to_tick, + LiquidityPosition, +) +from bittensor.utils.weight_utils import ( + generate_weight_hash, + U16_MAX, +) + +if TYPE_CHECKING: + from async_substrate_interface.types import ScaleObj + from bittensor_wallet import Wallet + from bittensor.core.axon import Axon + from async_substrate_interface import AsyncQueryMapResult + + +class AsyncSubtensor(SubtensorMixin): + """Asynchronous interface for interacting with the Bittensor blockchain. + + This class provides a thin layer over the Substrate Interface, offering a collection of frequently-used calls for + querying blockchain data, managing stakes, registering neurons, and interacting with the Bittensor network. + + + """ + + def __init__( + self, + network: Optional[str] = None, + config: Optional["Config"] = None, + log_verbose: bool = False, + fallback_endpoints: Optional[list[str]] = None, + retry_forever: bool = False, + _mock: bool = False, + archive_endpoints: Optional[list[str]] = None, + websocket_shutdown_timer: float = 5.0, + ): + """Initializes an AsyncSubtensor instance for blockchain interaction. + + Arguments: + network: The network name or type to connect to (e.g., "finney", "test"). If ``None``, uses the default + network from config. + config: Configuration object for the AsyncSubtensor instance. If ``None``, uses the default configuration. + log_verbose: Enables or disables verbose logging. Defaults to ``False``. + fallback_endpoints: List of fallback endpoints to use if default or provided network is not available. + Defaults to ``None``. + retry_forever: Whether to retry forever on connection errors. Defaults to ``False``. + _mock: Whether this is a mock instance. Mainly for testing purposes. Defaults to ``False``. + archive_endpoints: Similar to fallback_endpoints, but specifically only archive nodes. Will be used in + cases where you are requesting a block that is too old for your current (presumably lite) node. + Defaults to ``None``. + websocket_shutdown_timer: Amount of time, in seconds, to wait after the last response from the chain to + close the connection. Defaults to ``5.0``. + Returns: + None + + Raises: + ConnectionError: If unable to connect to the specified network. + ValueError: If invalid network or configuration parameters are provided. + Exception: Any other exceptions raised during setup or configuration. + + Typical usage example: + + import bittensor as bt + import asyncio + + async def main(): + async with bt.AsyncSubtensor(network="finney") as subtensor: + block_hash = await subtensor.get_block_hash() + + asyncio.run(main()) + """ + if config is None: + config = AsyncSubtensor.config() + self._config = copy.deepcopy(config) + self.chain_endpoint, self.network = AsyncSubtensor.setup_config( + network, self._config + ) + + self.log_verbose = log_verbose + self._check_and_log_network_settings() + + logging.debug( + f"Connecting to network: [blue]{self.network}[/blue], " + f"chain_endpoint: [blue]{self.chain_endpoint}[/blue]..." + ) + self.substrate = self._get_substrate( + fallback_endpoints=fallback_endpoints, + retry_forever=retry_forever, + _mock=_mock, + archive_endpoints=archive_endpoints, + ws_shutdown_timer=websocket_shutdown_timer, + ) + if self.log_verbose: + logging.info( + f"Connected to {self.network} network and {self.chain_endpoint}." + ) + + async def close(self): + """Closes the connection to the blockchain. + + Use this to explicitly clean up resources and close the network connection instead of waiting for garbage + collection. + + Returns: + None + + Example: + subtensor = AsyncSubtensor(network="finney") + await subtensor.initialize() + + # Use the subtensor... + balance = await subtensor.get_balance(address="5F...") + + # Close when done + await subtensor.close() + """ + if self.substrate: + await self.substrate.close() + + async def initialize(self): + """Initializes the connection to the blockchain. + + This method establishes the connection to the Bittensor blockchain and should be called after creating an + AsyncSubtensor instance before making any queries. + + Returns: + AsyncSubtensor: The initialized instance (self) for method chaining. + + Raises: + ConnectionError: If unable to connect to the blockchain due to timeout or connection refusal. + + Example: + subtensor = AsyncSubtensor(network="finney") + + # Initialize the connection + await subtensor.initialize() + + # Now you can make queries + balance = await subtensor.get_balance(address="5F...") + + # Or chain the initialization + subtensor = await AsyncSubtensor(network="finney").initialize() + """ + logging.info( + f"[magenta]Connecting to Substrate:[/magenta] [blue]{self}[/blue][magenta]...[/magenta]" + ) + try: + await self.substrate.initialize() + return self + except TimeoutError: + logging.error( + f"[red]Error[/red]: Timeout occurred connecting to substrate." + f" Verify your chain and network settings: {self}" + ) + raise ConnectionError + except (ConnectionRefusedError, ssl.SSLError) as error: + logging.error( + f"[red]Error[/red]: Connection refused when connecting to substrate. " + f"Verify your chain and network settings: {self}. Error: {error}" + ) + raise ConnectionError + + async def __aenter__(self): + logging.info( + f"[magenta]Connecting to Substrate:[/magenta] [blue]{self}[/blue][magenta]...[/magenta]" + ) + try: + await self.substrate.initialize() + return self + except TimeoutError: + logging.error( + f"[red]Error[/red]: Timeout occurred connecting to substrate." + f" Verify your chain and network settings: {self}" + ) + raise ConnectionError + except (ConnectionRefusedError, ssl.SSLError) as error: + logging.error( + f"[red]Error[/red]: Connection refused when connecting to substrate. " + f"Verify your chain and network settings: {self}. Error: {error}" + ) + raise ConnectionError + + async def __aexit__(self, exc_type, exc_val, exc_tb): + await self.substrate.close() + + async def determine_block_hash( + self, + block: Optional[int], + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> Optional[str]: + """Determine the appropriate block hash based on the provided parameters. + + Ensures that only one of the block specification parameters is used and returns the appropriate block hash + for blockchain queries. + + Arguments: + block: The block number to get the hash for. Do not specify if using block_hash or reuse_block. + block_hash: The hash of the blockchain block. Do not specify if using block or reuse_block. + reuse_block: Whether to reuse the last-used block hash. Do not set if using block or reuse_block. + + Returns: + Optional[str]: The block hash if one can be determined, None otherwise. + + Raises: + ValueError: If more than one of block, block_hash, or reuse_block is specified. + + Example: + # Get hash for specific block + block_hash = await subtensor.determine_block_hash(block=1000000) + + # Use provided block hash + hash = await subtensor.determine_block_hash(block_hash="0x1234...") + + # Reuse last block hash + hash = await subtensor.determine_block_hash(reuse_block=True) + """ + # Ensure that only one of the parameters is specified. + if sum(bool(x) for x in [block, block_hash, reuse_block]) > 1: + raise ValueError( + "Only one of ``block``, ``block_hash``, or ``reuse_block`` can be specified." + ) + + # Return the appropriate value. + if block_hash: + return block_hash + if block is not None: + return await self.get_block_hash(block) + return None + + async def encode_params( + self, + call_definition: dict[str, list["ParamWithTypes"]], + params: Union[list[Any], dict[str, Any]], + ) -> str: + """Encodes parameters into a hex string using their type definitions. + + This method takes a call definition (which specifies parameter types) and actual parameter values, then + encodes them into a hex string that can be used for blockchain transactions. + + Arguments: + call_definition: A dictionary containing parameter type definitions. Should have a "params" key with a + list of parameter definitions. + params: The actual parameter values to encode. Can be either a list (for positional parameters) or a + dictionary (for named parameters). + + Returns: + str: A hex-encoded string representation of the parameters. + + Raises: + ValueError: If a required parameter is missing from the params dictionary. + + Example: + # Define parameter types + call_def = { + "params": [ + {"name": "amount", "type": "u64"}, + {"name": "coldkey_ss58", "type": "str"} + ] + } + + # Encode parameters as a dictionary + params_dict = { + "amount": 1000000, + "coldkey_ss58": "5F..." + } + encoded = await subtensor.encode_params(call_definition=call_def, params=params_dict) + + # Or encode as a list (positional) + params_list = [1000000, "5F..."] + encoded = await subtensor.encode_params(call_definition=call_def, params=params_list) + """ + param_data = scalecodec.ScaleBytes(b"") + + for i, param in enumerate(call_definition["params"]): + scale_obj = await self.substrate.create_scale_object(param["type"]) + if isinstance(params, list): + param_data += scale_obj.encode(params[i]) + else: + if param["name"] not in params: + raise ValueError(f"Missing param {param['name']} in params dict.") + + param_data += scale_obj.encode(params[param["name"]]) + + return param_data.to_hex() + + async def get_hyperparameter( + self, + param_name: str, + netuid: int, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> Optional[Any]: + """Retrieves a specified hyperparameter for a specific subnet. + + This method queries the blockchain for subnet-specific hyperparameters such as difficulty, tempo, immunity + period, and other network configuration values. + + Arguments: + param_name: The name of the hyperparameter to retrieve (e.g., "Difficulty", "Tempo", "ImmunityPeriod"). + netuid: The unique identifier of the subnet. + block: The block number at which to retrieve the hyperparameter. Do not specify if using block_hash or + reuse_block. + block_hash: The hash of the blockchain block for the query. Do not specify if using block or reuse_block. + reuse_block: Whether to reuse the last-used block hash. Do not set if using block_hash or block. + + Returns: + The value of the specified hyperparameter if the subnet exists, None otherwise. + + Example: + # Get difficulty for subnet 1 + difficulty = await subtensor.get_hyperparameter(param_name="Difficulty", netuid=1) + + # Get tempo at a specific block + tempo = await subtensor.get_hyperparameter(param_name="Tempo", netuid=1, block=1000000) + + # Get immunity period using block hash + immunity = await subtensor.get_hyperparameter(param_name="ImmunityPeriod", netuid=1, block_hash="0x1234...") + """ + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + if not await self.subnet_exists( + netuid, block_hash=block_hash, reuse_block=reuse_block + ): + logging.error(f"subnet {netuid} does not exist") + return None + + result = await self.substrate.query( + module="SubtensorModule", + storage_function=param_name, + params=[netuid], + block_hash=block_hash, + reuse_block_hash=reuse_block, + ) + + return getattr(result, "value", result) + + def _get_substrate( + self, + fallback_endpoints: Optional[list[str]] = None, + retry_forever: bool = False, + _mock: bool = False, + archive_endpoints: Optional[list[str]] = None, + ws_shutdown_timer: float = 5.0, + ) -> Union[AsyncSubstrateInterface, RetryAsyncSubstrate]: + """Creates the Substrate instance based on provided arguments. + + This internal method creates either a standard AsyncSubstrateInterface or a RetryAsyncSubstrate depending on + the configuration parameters. + + Arguments: + fallback_endpoints: List of fallback endpoints to use if default or provided network is not available. + Defaults to ``None``. + retry_forever: Whether to retry forever on connection errors. Defaults to ``False``. + _mock: Whether this is a mock instance. Mainly for testing purposes. Defaults to ``False``. + archive_endpoints: Similar to fallback_endpoints, but specifically only archive nodes. Will be used in + cases where you are requesting a block that is too old for your current (presumably lite) node. Defaults + to ``None``. + ws_shutdown_timer: Amount of time, in seconds, to wait after the last response from the chain to close the + connection. + + Returns: + Either AsyncSubstrateInterface or RetryAsyncSubstrate. + """ + if fallback_endpoints or retry_forever or archive_endpoints: + return RetryAsyncSubstrate( + url=self.chain_endpoint, + fallback_chains=fallback_endpoints, + ss58_format=SS58_FORMAT, + type_registry=TYPE_REGISTRY, + retry_forever=retry_forever, + use_remote_preset=True, + chain_name="Bittensor", + _mock=_mock, + archive_nodes=archive_endpoints, + ws_shutdown_timer=ws_shutdown_timer, + ) + return AsyncSubstrateInterface( + url=self.chain_endpoint, + ss58_format=SS58_FORMAT, + type_registry=TYPE_REGISTRY, + use_remote_preset=True, + chain_name="Bittensor", + _mock=_mock, + ws_shutdown_timer=ws_shutdown_timer, + ) + + # Subtensor queries =========================================================================================== + + async def query_constant( + self, + module_name: str, + constant_name: str, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> Optional["ScaleObj"]: + """Retrieves a constant from the specified module on the Bittensor blockchain. + + This function is used to access fixed values defined within the blockchain's modules, which are essential for + understanding the network's configuration and rules. These include include critical network parameters such as + inflation rates, consensus rules, or validation thresholds, providing a deeper understanding of the Bittensor + network's operational parameters. + + Arguments: + module_name: The name of the module containing the constant (e.g., "Balances", "SubtensorModule"). + constant_name: The name of the constant to retrieve (e.g., "ExistentialDeposit"). + block: The blockchain block number at which to query the constant. Do not specify if using block_hash or + reuse_block. + block_hash: The hash of the blockchain block at which to query the constant. Do not specify if using + block or reuse_block. + reuse_block: Whether to reuse the blockchain block at which to query the constant. Defaults to ``False``. + + Returns: + Optional[async_substrate_interface.types.ScaleObj]: The value of the constant if found, ``None`` otherwise. + + Example: + # Get existential deposit constant + existential_deposit = await subtensor.query_constant( + module_name="Balances", + constant_name="ExistentialDeposit" + ) + + # Get constant at specific block + constant = await subtensor.query_constant( + module_name="SubtensorModule", + constant_name="SomeConstant", + block=1000000 + ) + """ + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + return await self.substrate.get_constant( + module_name=module_name, + constant_name=constant_name, + block_hash=block_hash, + reuse_block_hash=reuse_block, + ) + + async def query_map( + self, + module: str, + name: str, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + params: Optional[list] = None, + ) -> "AsyncQueryMapResult": + """Queries map storage from any module on the Bittensor blockchain. + + This function retrieves data structures that represent key-value mappings, essential for accessing complex and + structured data within the blockchain modules. + + Arguments: + module: The name of the module from which to query the map storage (e.g., "SubtensorModule", "System"). + name: The specific storage function within the module to query (e.g., "Bonds", "Weights"). + block: The blockchain block number at which to perform the query. Defaults to ``None`` (latest block). + block_hash: The hash of the block to retrieve the parameter from. Do not specify if using block or + reuse_block. + reuse_block: Whether to use the last-used block. Do not set if using block_hash or block. Defaults to + ``False``. + params: Parameters to be passed to the query. Defaults to ``None``. + + Returns: + AsyncQueryMapResult: A data structure representing the map storage if found, None otherwise. + + Example: + # Query bonds for subnet 1 + bonds = await subtensor.query_map(module="SubtensorModule", name="Bonds", params=[1]) + + # Query weights at specific block + weights = await subtensor.query_map(module="SubtensorModule", name="Weights", params=[1], block=1000000) + """ + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + result = await self.substrate.query_map( + module=module, + storage_function=name, + params=params, + block_hash=block_hash, + reuse_block_hash=reuse_block, + ) + return result + + async def query_map_subtensor( + self, + name: str, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + params: Optional[list] = None, + ) -> "AsyncQueryMapResult": + """Queries map storage from the Subtensor module on the Bittensor blockchain. This function is designed to + retrieve a map-like data structure, which can include various neuron-specific details or network-wide + attributes. + + Arguments: + name: The name of the map storage function to query. + block: The blockchain block number at which to perform the query. + block_hash: The hash of the block to retrieve the parameter from. Do not specify if using block or + reuse_block. + reuse_block: Whether to use the last-used block. Do not set if using block_hash or block. + params: A list of parameters to pass to the query function. + + Returns: + An object containing the map-like data structure, or ``None`` if not found. + + This function is particularly useful for analyzing and understanding complex network structures and + relationships within the Bittensor ecosystem, such as interneuronal connections and stake distributions. + """ + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + return await self.substrate.query_map( + module="SubtensorModule", + storage_function=name, + params=params, + block_hash=block_hash, + reuse_block_hash=reuse_block, + ) + + async def query_module( + self, + module: str, + name: str, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + params: Optional[list] = None, + ) -> Optional[Union["ScaleObj", Any]]: + """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. + + Arguments: + module: The name of the module from which to query data. + name: The name of the storage function within the module. + block: The blockchain block number at which to perform the query. + block_hash: The hash of the block to retrieve the parameter from. Do not specify if using block or + reuse_block. + reuse_block: Whether to use the last-used block. Do not set if using block_hash or block. + params: A list of parameters to pass to the query function. + + Returns: + An object containing the requested data if found, ``None`` otherwise. + + This versatile query function is key to accessing a wide range of data and insights from different parts of the + Bittensor blockchain, enhancing the understanding and analysis of the network's state and dynamics. + """ + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + return await self.substrate.query( + module=module, + storage_function=name, + params=params, + block_hash=block_hash, + reuse_block_hash=reuse_block, + ) + + async def query_runtime_api( + self, + runtime_api: str, + method: str, + params: Optional[Union[list[Any], dict[str, Any]]], + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> Optional[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. This function is essential for advanced users who need to + interact with specific runtime methods and decode complex data types. + + Arguments: + runtime_api: The name of the runtime API to query. + method: The specific method within the runtime API to call. + params: The parameters to pass to the method call. + block: the block number for this query. Do not specify if using block_hash or reuse_block. + block_hash: The hash of the blockchain block number at which to perform the query. Do not specify if using + block or reuse_block. + reuse_block: Whether to reuse the last-used block hash. Do not set if using block_hash or block. + + Returns: + The decoded result from the runtime API call, or ``None`` if the call fails. + + This function enables access to the deeper layers of the Bittensor blockchain, allowing for detailed and + specific interactions with the network's runtime environment. + """ + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + if not block_hash and reuse_block: + block_hash = self.substrate.last_block_hash + result = await self.substrate.runtime_call( + runtime_api, method, params, block_hash + ) + return result.value + + async def query_subtensor( + self, + name: str, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + params: Optional[list] = None, + ) -> Optional[Union["ScaleObj", Any]]: + """Queries named storage from the Subtensor module on the Bittensor blockchain. This function is used to + retrieve specific data or parameters from the blockchain, such as stake, rank, or other neuron-specific + attributes. + + Arguments: + name: The name of the storage function to query. + block: The blockchain block number at which to perform the query. + block_hash: The hash of the block to retrieve the parameter from. Do not specify if using block or + reuse_block. + reuse_block: Whether to use the last-used block. Do not set if using block_hash or block. + params: A list of parameters to pass to the query function. + + Returns: + query_response: An object containing the requested data. + + This query function is essential for accessing detailed information about the network and its neurons, providing + valuable insights into the state and dynamics of the Bittensor ecosystem. + """ + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + return await self.substrate.query( + module="SubtensorModule", + storage_function=name, + params=params, + block_hash=block_hash, + reuse_block_hash=reuse_block, + ) + + async def state_call( + self, + method: str, + data: str, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> dict[Any, Any]: + """Makes a state call to the Bittensor blockchain, allowing for direct queries of the blockchain's state. + This function is typically used for advanced queries that require specific method calls and data inputs. + + Arguments: + method: The method name for the state call. + data: The data to be passed to the method. + block: The blockchain block number at which to perform the state call. + block_hash: The hash of the block to retrieve the parameter from. Do not specify if using block or + reuse_block. + reuse_block: Whether to use the last-used block. Do not set if using block_hash or block. + + Returns: + result (dict[Any, Any]): The result of the rpc call. + + The state call function provides a more direct and flexible way of querying blockchain data, useful for specific + use cases where standard queries are insufficient. + """ + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + return await self.substrate.rpc_request( + method="state_call", + params=[method, data], + block_hash=block_hash, + reuse_block_hash=reuse_block, + ) + + # Common subtensor methods ========================================================================================= + + @property + async def block(self): + """Provides an asynchronous property to retrieve the current block.""" + return await self.get_current_block() + + async def all_subnets( + self, + block_number: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> Optional[list[DynamicInfo]]: + """Queries the blockchain for comprehensive information about all subnets, including their dynamic parameters + and operational status. + + Arguments: + block_number: The block number to query the subnet information from. Do not specify if using block_hash or + reuse_block. + block_hash: The hash of the blockchain block number for the query. Do not specify if using reuse_block or + block. + reuse_block: Whether to reuse the last-used blockchain block hash. Do not set if using block_hash or block. + + Returns: + Optional[list[DynamicInfo]]: A list of DynamicInfo objects, each containing detailed information about a + subnet, or None if the query fails. + + Example: + # Get all subnets at current block + subnets = await subtensor.all_subnets() + """ + block_hash = await self.determine_block_hash( + block=block_number, block_hash=block_hash, reuse_block=reuse_block + ) + if not block_hash and reuse_block: + block_hash = self.substrate.last_block_hash + + query, 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, + ) + decoded = query.decode() + + if not isinstance(subnet_prices, (SubstrateRequestException, ValueError)): + for sn in decoded: + sn.update( + {"price": subnet_prices.get(sn["netuid"], Balance.from_tao(0))} + ) + else: + logging.warning( + f"Unable to fetch subnet prices for block {block_number}, block hash {block_hash}: {subnet_prices}" + ) + return DynamicInfo.list_from_dicts(decoded) + + async def blocks_since_last_step( + self, + netuid: int, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> Optional[int]: + """Queries the blockchain to determine how many blocks have passed since the last epoch step for a specific + subnet. + + Arguments: + netuid: The unique identifier of the subnetwork. + block: The block number for this query. Do not specify if using block_hash or reuse_block. + block_hash: The hash of the blockchain block number for the query. Do not specify if using reuse_block or + block. + reuse_block: Whether to reuse the last-used blockchain block hash. Do not set if using block_hash or block. + + Returns: + The number of blocks since the last step in the subnet, or None if the query fails. + + Example: + # Get blocks since last step for subnet 1 + blocks = await subtensor.blocks_since_last_step(netuid=1) + + # Get blocks since last step at specific block + blocks = await subtensor.blocks_since_last_step(netuid=1, block=1000000) + """ + query = await self.query_subtensor( + name="BlocksSinceLastStep", + block=block, + block_hash=block_hash, + reuse_block=reuse_block, + params=[netuid], + ) + return query.value if query is not None and hasattr(query, "value") else query + + async def blocks_since_last_update(self, netuid: int, uid: int) -> Optional[int]: + """Returns the number of blocks since the last update, or ``None`` if the subnetwork or UID does not exist. + + Arguments: + netuid: The unique identifier of the subnetwork. + uid: The unique identifier of the neuron. + + Returns: + Optional[int]: The number of blocks since the last update, or None if the subnetwork or UID does not exist. + + Example: + # Get blocks since last update for UID 5 in subnet 1 + blocks = await subtensor.blocks_since_last_update(netuid=1, uid=5) + + # Check if neuron needs updating + blocks_since_update = await subtensor.blocks_since_last_update(netuid=1, uid=10) + """ + call = await self.get_hyperparameter(param_name="LastUpdate", netuid=netuid) + return None if call is None else await self.get_current_block() - int(call[uid]) + + async def bonds( + self, + netuid: int, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> list[tuple[int, list[tuple[int, int]]]]: + """Retrieves the bond distribution set by subnet validators within a specific subnet. + + Bonds represent the "investment" a subnet validator has made in evaluating a specific subnet miner. This + bonding mechanism is integral to the Yuma Consensus' design intent of incentivizing high-quality performance + by subnet miners, and honest evaluation by subnet validators. + + Arguments: + netuid: The unique identifier of the subnet. + block: The block number for this query. Do not specify if using block_hash or reuse_block. + block_hash: The hash of the block for the query. Do not specify if using reuse_block or block. + reuse_block: Whether to reuse the last-used block hash. Do not set if using block_hash or block. + + Returns: + List of tuples mapping each neuron's UID to its bonds with other neurons. + + Example: + # Get bonds for subnet 1 at block 1000000 + bonds = await subtensor.bonds(netuid=1, block=1000000) + + Notes: + - See + - See + """ + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + b_map_encoded = await self.substrate.query_map( + module="SubtensorModule", + storage_function="Bonds", + params=[netuid], + 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)) + + return b_map + + async def commit_reveal_enabled( + self, + netuid: int, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> bool: + """Check if commit-reveal mechanism is enabled for a given subnet at a specific block. + + The commit reveal feature is designed to solve the weight-copying problem by giving would-be weight-copiers + access only to stale weights. Copying stale weights should result in subnet validators departing from consensus. + + Arguments: + netuid: The unique identifier of the subnet for which to check the commit-reveal mechanism. + block: The block number to query. Do not specify if using block_hash or reuse_block. + block_hash: The block hash at which to check the parameter. Do not set if using block or reuse_block. + reuse_block: Whether to reuse the last-used block hash. Do not set if using block_hash or block. + + Returns: + bool: True if commit-reveal mechanism is enabled, False otherwise. + + Example: + # Check if commit-reveal is enabled for subnet 1 + enabled = await subtensor.commit_reveal_enabled(netuid=1) + + # Check at specific block + enabled = await subtensor.commit_reveal_enabled(netuid=1, block=1000000) + + Notes: + See also: + """ + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + call = 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 + + async def difficulty( + self, + netuid: int, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> Optional[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 + validation processes. The difficulty directly impacts the network's security and integrity by setting the + computational effort required for validating transactions and participating in the network's consensus + mechanism. + + Arguments: + netuid: The unique identifier of the subnet. + block: The block number for the query. Do not specify if using block_hash or reuse_block. + block_hash: The hash of the block to retrieve the parameter from. Do not specify if using block or + reuse_block. + reuse_block: Whether to use the last-used block. Do not set if using block_hash or block. + + Returns: + Optional[int]: The value of the 'Difficulty' hyperparameter if the subnet exists, None otherwise. + + Example: + # Get difficulty for subnet 1 + difficulty = await subtensor.difficulty(netuid=1) + + # Get difficulty at specific block + difficulty = await subtensor.difficulty(netuid=1, block=1000000) + + Notes: + See also: + """ + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + call = await self.get_hyperparameter( + param_name="Difficulty", + netuid=netuid, + block_hash=block_hash, + reuse_block=reuse_block, + ) + if call is None: + return None + return int(call) + + async def does_hotkey_exist( + self, + hotkey_ss58: str, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> bool: + """Returns true if the hotkey is known by the chain and there are accounts. + + This method queries the SubtensorModule's Owner storage function to determine if the hotkey is registered. + + Arguments: + hotkey_ss58: The SS58 address of the hotkey. + block: The block number for this query. Do not specify if using block_hash or reuse_block. + block_hash: The hash of the block number to check the hotkey against. Do not specify if using reuse_block + or block. + reuse_block: Whether to reuse the last-used blockchain hash. Do not set if using block_hash or block. + + Returns: + bool: True if the hotkey is known by the chain and there are accounts, False otherwise. + + Example: + # Check if hotkey exists + exists = await subtensor.does_hotkey_exist(hotkey_ss58="5F...") + + # Check at specific block + exists = await subtensor.does_hotkey_exist(hotkey_ss58="5F...", block=1000000) + """ + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + result = 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 + + async def get_all_subnets_info( + self, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> list["SubnetInfo"]: + """Retrieves detailed information about all subnets within the Bittensor network. + + This function provides comprehensive data on each subnet, including its characteristics and operational + parameters. + + Arguments: + block: The block number for the query. + block_hash: The block hash for the query. + reuse_block: Whether to reuse the last-used block hash. + + Returns: + list[SubnetInfo]: A list of SubnetInfo objects, each containing detailed information about a subnet. + + Example: + # Get all subnet information + subnets = await subtensor.get_all_subnets_info() + + # Get at specific block + subnets = await subtensor.get_all_subnets_info(block=1000000) + + # Iterate over subnet information + for subnet in subnets: + print(f"Subnet {subnet.netuid}: {subnet.name}") + + Note: + Gaining insights into the subnets' details assists in understanding the network's composition, the roles + of different subnets, and their unique features. + + Notes: + See also: + """ + result, prices = await asyncio.gather( + self.query_runtime_api( + runtime_api="SubnetInfoRuntimeApi", + method="get_subnets_info_v2", + params=[], + block=block, + block_hash=block_hash, + reuse_block=reuse_block, + ), + self.get_subnet_prices( + block=block, block_hash=block_hash, reuse_block=reuse_block + ), + return_exceptions=True, + ) + if not result: + return [] + + if not isinstance(prices, (SubstrateRequestException, ValueError)): + for subnet in result: + subnet.update({"price": prices.get(subnet["netuid"], 0)}) + else: + logging.warning( + f"Unable to fetch subnet prices for block {block}, block hash {block_hash}: {prices}" + ) + + return SubnetInfo.list_from_dicts(result) + + async def get_balance( + self, + address: str, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> Balance: + """Retrieves the balance for given coldkey. + + This method queries the System module's Account storage to get the current balance of a coldkey address. The + balance represents the amount of TAO tokens held by the specified address. + + Arguments: + address: The coldkey address in SS58 format. + block: The block number for the query. + block_hash: The block hash for the query. + reuse_block: Whether to reuse the last-used block hash. + + Returns: + Balance: The balance object containing the account's TAO balance. + + Example: + # Get balance for an address + balance = await subtensor.get_balance(address="5F...") + print(f"Balance: {balance.tao} TAO") + + # Get balance at specific block + balance = await subtensor.get_balance(address="5F...", block=1000000) + """ + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + balance = 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"]) + + async def get_balances( + self, + *addresses: str, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> dict[str, Balance]: + """Retrieves the balance for given coldkey(s). + + This method efficiently queries multiple coldkey addresses in a single batch operation, returning a dictionary + mapping each address to its corresponding balance. This is more efficient than calling get_balance multiple + times. + + Arguments: + *addresses: Variable number of coldkey addresses in SS58 format. + block: The block number for the query. + block_hash: The block hash for the query. + reuse_block: Whether to reuse the last-used block hash. + + Returns: + dict[str, Balance]: A dictionary mapping each address to its Balance object. + + Example: + # Get balances for multiple addresses + balances = await subtensor.get_balances("5F...", "5G...", "5H...") + """ + if reuse_block: + block_hash = self.substrate.last_block_hash + elif block_hash is None and block is None: + # Neither block nor block_hash provided, default to head + block_hash = await self.get_block_hash() + else: + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + calls = [ + ( + await self.substrate.create_storage_key( + "System", "Account", [address], block_hash=block_hash + ) + ) + for address in addresses + ] + batch_call = await self.substrate.query_multi(calls, block_hash=block_hash) + results = {} + for item in batch_call: + value = item[1] or {"data": {"free": 0}} + results.update({item[0].params[0]: Balance(value["data"]["free"])}) + return results + + async def get_current_block(self) -> int: + """Returns the current block number on the Bittensor blockchain. + + This function provides the latest block number, indicating the most recent state of the blockchain. Knowing + the current block number is essential for querying real-time data and performing time-sensitive operations on + the blockchain. It serves as a reference point for network activities and data synchronization. + + Returns: + int: The current chain block number. + + Example: + # Get current block number + current_block = await subtensor.get_current_block() + print(f"Current block: {current_block}") + + block = await subtensor.get_current_block() + if block > 1000000: + print("Network has progressed past block 1M") + + Notes: + See also: + """ + return await self.substrate.get_block_number(None) + + @a.lru_cache(maxsize=128) + async def _get_block_hash(self, block_id: int): + return await self.substrate.get_block_hash(block_id) + + async def get_block_hash(self, block: Optional[int] = None) -> str: + """Retrieves the hash of a specific block on the Bittensor blockchain. + + The block hash is a unique identifier representing the cryptographic hash of the block's content, ensuring its + integrity and immutability. It is a fundamental aspect of blockchain technology, providing a secure reference + to each block's data. It is crucial for verifying transactions, ensuring data consistency, and maintaining the + trustworthiness of the blockchain. + + Arguments: + block: The block number for which the hash is to be retrieved. If ``None``, returns the latest block hash. + + Returns: + str: The cryptographic hash of the specified block. + + Example: + # Get hash for specific block + block_hash = await subtensor.get_block_hash(block=1000000) + print(f"Block 1000000 hash: {block_hash}") + + # Get latest block hash + latest_hash = await subtensor.get_block_hash() + print(f"Latest block hash: {latest_hash}") + + Notes: + See also: + """ + if block is not None: + return await self._get_block_hash(block) + else: + return await self.substrate.get_chain_head() + + async def get_parents( + self, + hotkey: str, + netuid: int, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> list[tuple[float, str]]: + """This method retrieves the parent of a given hotkey and netuid. It queries the SubtensorModule's ParentKeys + storage function to get the children and formats them before returning as a tuple. + + Arguments: + hotkey: The child hotkey SS58. + netuid: The netuid value. + block: The block number for which the children are to be retrieved. + block_hash: The hash of the block to retrieve the subnet unique identifiers from. + reuse_block: Whether to reuse the last-used block hash. + + Returns: + A list of formatted parents [(proportion, parent)] + """ + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + parents = await self.substrate.query( + module="SubtensorModule", + storage_function="ParentKeys", + params=[hotkey, netuid], + block_hash=block_hash, + reuse_block_hash=reuse_block, + ) + if parents: + formatted_parents = [] + for proportion, parent 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 + + return [] + + async def get_children( + self, + hotkey: str, + netuid: int, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> tuple[bool, list[tuple[float, str]], str]: + """Retrieves the children of a given hotkey and netuid. + + This method queries the SubtensorModule's ChildKeys storage function to get the children and formats them before + returning as a tuple. It provides information about the child neurons that a validator has set for weight + distribution. + + Arguments: + hotkey: The hotkey value. + netuid: The netuid value. + block: The block number for which the children are to be retrieved. + block_hash: The hash of the block to retrieve the subnet unique identifiers from. + reuse_block: Whether to reuse the last-used block hash. + + Returns: + tuple[bool, list[tuple[float, str]], str]: A tuple containing a boolean indicating success or failure, a + list of formatted children with their proportions, and an error message (if applicable). + + Example: + # Get children for a hotkey in subnet 1 + success, children, error = await subtensor.get_children(hotkey="5F...", netuid=1) + + if success: + for proportion, child_hotkey in children: + print(f"Child {child_hotkey}: {proportion}") + """ + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + try: + children = await self.substrate.query( + module="SubtensorModule", + storage_function="ChildKeys", + params=[hotkey, netuid], + block_hash=block_hash, + reuse_block_hash=reuse_block, + ) + if children: + formatted_children = [] + for proportion, 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, "" + else: + return True, [], "" + except SubstrateRequestException as e: + return False, [], format_error_message(e) + + async def get_children_pending( + self, + hotkey: str, + netuid: int, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> tuple[ + list[tuple[float, str]], + int, + ]: + """Retrieves the pending children of a given hotkey and netuid. + + This method queries the SubtensorModule's PendingChildKeys storage function to get children that are pending + approval or in a cooldown period. These are children that have been proposed but not yet finalized. + + Arguments: + hotkey: The hotkey value. + netuid: The netuid value. + block: The block number for which the children are to be retrieved. + block_hash: The hash of the block to retrieve the subnet unique identifiers from. + reuse_block: Whether to reuse the last-used block hash. + + Returns: + list[tuple[float, str]]: A list of children with their proportions. + int: The cool-down block number. + """ + + response = await self.substrate.query( + module="SubtensorModule", + storage_function="PendingChildKeys", + params=[netuid, hotkey], + block_hash=await self.determine_block_hash( + block, + block_hash, + reuse_block, + ), + reuse_block_hash=reuse_block, + ) + children, cooldown = response.value + + return ( + [ + ( + u64_normalized_float(proportion), + decode_account_id(child[0]), + ) + for proportion, child in children + ], + cooldown, + ) + + async def get_commitment( + self, + netuid: int, + uid: int, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> str: + """Retrieves the on-chain commitment for a specific neuron in the Bittensor network. + + This method retrieves the commitment data that a neuron has published to the blockchain. Commitments are used in + the commit-reveal mechanism for secure weight setting and other network operations. + + Arguments: + netuid: The unique identifier of the subnetwork. + uid: The unique identifier of the neuron. + block: The block number to retrieve the commitment from. If None, the latest block is used. + Default is None. + block_hash: The hash of the block to retrieve the subnet unique identifiers from. + reuse_block: Whether to reuse the last-used block hash. + + Returns: + str: The commitment data as a string. + + Example: + # Get commitment for UID 5 in subnet 1 + commitment = await subtensor.get_commitment(netuid=1, uid=5) + print(f"Commitment: {commitment}") + + # Get commitment at specific block + commitment = await subtensor.get_commitment( + netuid=1, + uid=5, + block=1000000 + ) + """ + metagraph = await self.metagraph(netuid) + try: + hotkey = metagraph.hotkeys[uid] # type: ignore + except IndexError: + logging.error( + "Your uid is not in the hotkeys. Please double-check your UID." + ) + return "" + + metadata = await get_metadata( + self, netuid, hotkey, block, block_hash, reuse_block + ) + try: + return decode_metadata(metadata) + except TypeError: + return "" + + async def get_last_commitment_bonds_reset_block( + self, netuid: int, uid: int + ) -> Optional[int]: + """ + Retrieves the last block number when the bonds reset were triggered by publish_metadata for a specific neuron. + + Arguments: + netuid: The unique identifier of the subnetwork. + uid: The unique identifier of the neuron. + + Returns: + Optional[int]: The block number when the bonds were last reset, or None if not found. + """ + + metagraph = await self.metagraph(netuid) + try: + hotkey = metagraph.hotkeys[uid] + except IndexError: + logging.error( + "Your uid is not in the hotkeys. Please double-check your UID." + ) + return None + block = await get_last_bonds_reset(self, netuid, hotkey) + try: + return decode_block(block) + except TypeError: + return None + + async def get_all_commitments( + self, + netuid: int, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> dict[str, str]: + """Retrieves the on-chain commitments for a specific subnet in the Bittensor network. + + This method retrieves all commitment data for all neurons in a specific subnet. This is useful for analyzing the + commit-reveal patterns across an entire subnet. + + Arguments: + netuid: The unique identifier of the subnetwork. + block: The block number to retrieve the commitment from. If None, the latest block is used. + Default is None. + block_hash: The hash of the block to retrieve the subnet unique identifiers from. + reuse_block: Whether to reuse the last-used block hash. + + Returns: + dict[str, str]: A mapping of the ss58:commitment with the commitment as a string. + + Example: + # Get all commitments for subnet 1 + commitments = await subtensor.get_all_commitments(netuid=1) + + # Iterate over all commitments + for hotkey, commitment in commitments.items(): + print(f"Hotkey {hotkey}: {commitment}") + """ + query = await self.query_map( + module="Commitments", + name="CommitmentOf", + params=[netuid], + block=block, + block_hash=block_hash, + reuse_block=reuse_block, + ) + result = {} + async for id_, value in query: + result[decode_account_id(id_[0])] = decode_metadata(value.value) + return result + + async def get_revealed_commitment_by_hotkey( + self, + netuid: int, + hotkey_ss58_address: Optional[str] = None, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> Optional[tuple[tuple[int, str], ...]]: + """Returns hotkey related revealed commitment for a given netuid. + + Arguments: + netuid: The unique identifier of the subnetwork. + block: The block number to retrieve the commitment from. Default is ``None``. + hotkey_ss58_address: The ss58 address of the committee member. + block_hash: The hash of the block to retrieve the subnet unique identifiers from. + reuse_block: Whether to reuse the last-used block hash. + + Returns: + result (tuple[int, str): A tuple of reveal block and commitment message. + """ + if not is_valid_ss58_address(address=hotkey_ss58_address): + raise ValueError(f"Invalid ss58 address {hotkey_ss58_address} provided.") + + query = await self.query_module( + module="Commitments", + name="RevealedCommitments", + params=[netuid, hotkey_ss58_address], + block=block, + block_hash=block_hash, + reuse_block=reuse_block, + ) + if query is None: + return None + return tuple(decode_revealed_commitment(pair) for pair in query) + + async def get_revealed_commitment( + self, + netuid: int, + uid: int, + block: Optional[int] = None, + ) -> Optional[tuple[tuple[int, str], ...]]: + """Returns uid related revealed commitment for a given netuid. + + Arguments: + netuid: The unique identifier of the subnetwork. + uid: The neuron uid to retrieve the commitment from. + block: The block number to retrieve the commitment from. Default is ``None``. + + Returns: + result (Optional[tuple[int, str]]: A tuple of reveal block and commitment message. + + Example of result: + ( (12, "Alice message 1"), (152, "Alice message 2") ) + ( (12, "Bob message 1"), (147, "Bob message 2") ) + """ + try: + meta_info = await self.get_metagraph_info(netuid, block=block) + if meta_info: + hotkey_ss58_address = meta_info.hotkeys[uid] + else: + raise ValueError(f"Subnet with netuid {netuid} does not exist.") + except IndexError: + raise ValueError(f"Subnet {netuid} does not have a neuron with uid {uid}.") + + return await self.get_revealed_commitment_by_hotkey( + netuid=netuid, hotkey_ss58_address=hotkey_ss58_address, block=block + ) + + async def get_all_revealed_commitments( + self, + netuid: int, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> dict[str, tuple[tuple[int, str], ...]]: + """Returns all revealed commitments for a given netuid. + + Arguments: + netuid: The unique identifier of the subnetwork. + block: The block number to retrieve the commitment from. Default is ``None``. + block_hash: The hash of the block to retrieve the subnet unique identifiers from. + reuse_block: Whether to reuse the last-used block hash. + + Returns: + result: A dictionary of all revealed commitments in view {ss58_address: (reveal block, commitment message)}. + + Example of result: + { + "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY": ( (12, "Alice message 1"), (152, "Alice message 2") ), + "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty": ( (12, "Bob message 1"), (147, "Bob message 2") ), + } + """ + query = await self.query_map( + module="Commitments", + name="RevealedCommitments", + params=[netuid], + block=block, + block_hash=block_hash, + reuse_block=reuse_block, + ) + + result = {} + async for pair in query: + hotkey_ss58_address, commitment_message = ( + decode_revealed_commitment_with_hotkey(pair) + ) + result[hotkey_ss58_address] = commitment_message + return result + + async def get_current_weight_commit_info( + self, + netuid: int, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> list[tuple[str, str, int]]: + """ + Retrieves CRV3 weight commit information for a specific subnet. + + Arguments: + netuid: The unique identifier of the subnet. + block: The blockchain block number for the query. Default is ``None``. + block_hash: The hash of the block to retrieve the subnet unique identifiers from. + reuse_block: Whether to reuse the last-used block hash. + + Returns: + A list of commit details, where each item contains: + - ss58_address: The address of the committer. + - commit_message: The commit message. + - reveal_round: The round when the commitment was revealed. + + The list may be empty if there are no commits found. + """ + deprecated_message( + message="The method `get_current_weight_commit_info` is deprecated and will be removed in version 10.0.0. " + "Use `get_current_weight_commit_info_v2` instead." + ) + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + result = await self.substrate.query_map( + module="SubtensorModule", + storage_function="CRV3WeightCommits", + params=[netuid], + block_hash=block_hash, + reuse_block_hash=reuse_block, + ) + + commits = result.records[0][1] if result.records else [] + return [WeightCommitInfo.from_vec_u8(commit) for commit in commits] + + async def get_current_weight_commit_info_v2( + self, + netuid: int, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> list[tuple[str, int, str, int]]: + """ + Retrieves CRV3 weight commit information for a specific subnet. + + Arguments: + netuid: The unique identifier of the subnet. + block: The blockchain block number for the query. Default is ``None``. + block_hash: The hash of the block to retrieve the subnet unique identifiers from. + reuse_block: Whether to reuse the last-used block hash. + + Returns: + A list of commit details, where each item contains: + - ss58_address: The address of the committer. + - commit_block: The block number when the commitment was made. + - commit_message: The commit message. + - reveal_round: The round when the commitment was revealed. + + The list may be empty if there are no commits found. + """ + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + result = await self.substrate.query_map( + module="SubtensorModule", + storage_function="CRV3WeightCommitsV2", + params=[netuid], + block_hash=block_hash, + reuse_block_hash=reuse_block, + ) + + commits = result.records[0][1] if result.records else [] + return [WeightCommitInfo.from_vec_u8_v2(commit) for commit in commits] + + async def get_delegate_by_hotkey( + self, + hotkey_ss58: str, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> Optional[DelegateInfo]: + """ + Retrieves detailed information about a delegate neuron based on its hotkey. This function provides a + comprehensive view of the delegate's status, including its stakes, nominators, and reward distribution. + + Arguments: + hotkey_ss58: The ``SS58`` address of the delegate's hotkey. + block: The blockchain block number for the query. + block_hash: The hash of the block to retrieve the subnet unique identifiers from. + reuse_block: Whether to reuse the last-used block hash. + + Returns: + Optional[DelegateInfo]: Detailed information about the delegate neuron, ``None`` if not found. + + This function is essential for understanding the roles and influence of delegate neurons within the Bittensor + network's consensus and governance structures. + """ + + result = await self.query_runtime_api( + runtime_api="DelegateInfoRuntimeApi", + method="get_delegate", + params=[hotkey_ss58], + block=block, + block_hash=block_hash, + reuse_block=reuse_block, + ) + + if not result: + return None + + return DelegateInfo.from_dict(result) + + async def get_delegate_identities( + self, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> dict[str, ChainIdentity]: + """ + Fetches delegates identities from the chain. + + Arguments: + block: The blockchain block number for the query. + block_hash: the hash of the blockchain block for the query + reuse_block: Whether to reuse the last-used blockchain block hash. + + Returns: + Dict {ss58: ChainIdentity, ...} + + """ + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + identities = await self.substrate.query_map( + 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), + ) + async for ss58_address, identity in identities + } + + async def get_delegate_take( + self, + hotkey_ss58: str, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> float: + """ + Retrieves the delegate 'take' percentage for a neuron identified by its hotkey. The 'take' represents the + percentage of rewards that the delegate claims from its nominators' stakes. + + Arguments: + hotkey_ss58: The ``SS58`` address of the neuron's hotkey. + block: The blockchain block number for the query. + block_hash: The hash of the block to retrieve the subnet unique identifiers from. + reuse_block: Whether to reuse the last-used block hash. + + Returns: + float: The delegate take percentage. + + The delegate take is a critical parameter in the network's incentive structure, influencing the distribution of + rewards among neurons and their nominators. + """ + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + result = 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 + + async def get_delegated( + self, + coldkey_ss58: str, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> list[DelegatedInfo]: + """ + Retrieves a list of delegates and their associated stakes for a given coldkey. This function identifies the + delegates that a specific account has staked tokens on. + + Arguments: + coldkey_ss58: The ``SS58`` address of the account's coldkey. + block: The blockchain block number for the query. + block_hash: The hash of the blockchain block number for the query. + reuse_block: Whether to reuse the last-used blockchain block hash. + + Returns: + A list containing the delegated information for the specified coldkey. + + This function is important for account holders to understand their stake allocations and their involvement in + the network's delegation and consensus mechanisms. + """ + + result = await self.query_runtime_api( + runtime_api="DelegateInfoRuntimeApi", + method="get_delegated", + params=[coldkey_ss58], + block=block, + block_hash=block_hash, + reuse_block=reuse_block, + ) + + if not result: + return [] + + return DelegatedInfo.list_from_dicts(result) + + async def get_delegates( + self, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> list[DelegateInfo]: + """ + Fetches all delegates on the chain + + Arguments: + block: The blockchain block number for the query. + block_hash: hash of the blockchain block number for the query. + reuse_block: whether to reuse the last-used block hash. + + Returns: + List of DelegateInfo objects, or an empty list if there are no delegates. + """ + result = await self.query_runtime_api( + runtime_api="DelegateInfoRuntimeApi", + method="get_delegates", + params=[], + block=block, + block_hash=block_hash, + reuse_block=reuse_block, + ) + if result: + return DelegateInfo.list_from_dicts(result) + else: + return [] + + async def get_existential_deposit( + self, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> 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. + Accounts with balances below this threshold can be reaped to conserve network resources. + + Arguments: + block: The blockchain block number for the query. + block_hash: Block hash at which to query the deposit amount. If ``None``, the current block is used. + reuse_block: Whether to reuse the last-used blockchain block hash. + + Returns: + The existential deposit amount. + + The existential deposit is a fundamental economic parameter in the Bittensor network, ensuring efficient use of + storage and preventing the proliferation of dust accounts. + """ + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + result = 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)) + + async def get_hotkey_owner( + self, + hotkey_ss58: str, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> Optional[str]: + """ + Retrieves the owner of the given hotkey at a specific block hash. + This function queries the blockchain for the owner of the provided hotkey. If the hotkey does not exist at the + specified block hash, it returns None. + + Arguments: + hotkey_ss58: The SS58 address of the hotkey. + block: The blockchain block number for the query. + block_hash: The hash of the block at which to check the hotkey ownership. + reuse_block: Whether to reuse the last-used blockchain hash. + + Returns: + Optional[str]: The SS58 address of the owner if the hotkey exists, or None if it doesn't. + + Notes: + See also: + - + - + - + """ + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + hk_owner_query = 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: + exists = await self.does_hotkey_exist(hotkey_ss58, block_hash=block_hash) + hotkey_owner = hk_owner_query if exists else None + return hotkey_owner + + async def get_minimum_required_stake(self): + """ + Returns the minimum required stake for nominators in the Subtensor network. + + Returns: + Balance: The minimum required stake as a Balance object. + """ + result = await self.substrate.query( + module="SubtensorModule", storage_function="NominatorMinRequiredStake" + ) + + return Balance.from_rao(getattr(result, "value", 0)) + + async def get_metagraph_info( + self, + netuid: int, + field_indices: Optional[Union[list[SelectiveMetagraphIndex], list[int]]] = None, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> Optional[MetagraphInfo]: + """ + Retrieves full or partial metagraph information for the specified subnet (netuid). + + A metagraph is a data structure that contains comprehensive information about the current state of a subnet, + including detailed information on all the nodes (neurons) such as subnet validator stakes and subnet weights + in the subnet. Metagraph aids in calculating emissions. + + Arguments: + netuid: The unique identifier of the subnet to query. + field_indices: An optional list of SelectiveMetagraphIndex or int values specifying which fields to + retrieve. If not provided, all available fields will be returned. + block: the block number at which to retrieve the hyperparameter. Do not specify if using block_hash or + reuse_block + block_hash: The hash of blockchain block number for the query. Do not specify if using + block or reuse_block + reuse_block: Whether to reuse the last-used block hash. Do not set if using block_hash or block. + + Returns: + Optional[MetagraphInfo]: A MetagraphInfo object containing the requested subnet data, or None if the subnet + with the given netuid does not exist. + + Example: + meta_info = await subtensor.get_metagraph_info(netuid=2) + + partial_meta_info = await subtensor.get_metagraph_info( + netuid=2, + field_indices=[SelectiveMetagraphIndex.Name, SelectiveMetagraphIndex.OwnerHotkeys] + ) + + Notes: + See also: + - + - + """ + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + if not block_hash and reuse_block: + block_hash = self.substrate.last_block_hash + + if field_indices: + if isinstance(field_indices, list) and all( + isinstance(f, (SelectiveMetagraphIndex, int)) for f in field_indices + ): + indexes = [ + f.value if isinstance(f, SelectiveMetagraphIndex) else f + for f in field_indices + ] + else: + raise ValueError( + "`field_indices` must be a list of SelectiveMetagraphIndex enums or ints." + ) + + query = await self.substrate.runtime_call( + "SubnetInfoRuntimeApi", + "get_selective_metagraph", + params=[netuid, indexes if 0 in indexes else [0] + indexes], + block_hash=block_hash, + ) + else: + query = await self.substrate.runtime_call( + "SubnetInfoRuntimeApi", + "get_metagraph", + params=[netuid], + block_hash=block_hash, + ) + + if query.value is None: + logging.error(f"Subnet {netuid} does not exist.") + return None + + return MetagraphInfo.from_dict(query.value) + + async def get_all_metagraphs_info( + self, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> list[MetagraphInfo]: + """ + Retrieves a list of MetagraphInfo objects for all subnets + + Arguments: + block: the block number at which to retrieve the hyperparameter. Do not specify if using block_hash or + reuse_block + block_hash: The hash of blockchain block number for the query. Do not specify if using + block or reuse_block + reuse_block: Whether to reuse the last-used block hash. Do not set if using block_hash or block. + + Returns: + MetagraphInfo dataclass + + Notes: + See also: See + """ + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + if not block_hash and reuse_block: + block_hash = self.substrate.last_block_hash + query = await self.substrate.runtime_call( + "SubnetInfoRuntimeApi", + "get_all_metagraphs", + block_hash=block_hash, + ) + return MetagraphInfo.list_from_dicts(query.decode()) + + async def get_netuids_for_hotkey( + self, + hotkey_ss58: str, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> list[int]: + """ + Retrieves a list of subnet UIDs (netuids) for which a given hotkey is a member. This function identifies the + specific subnets within the Bittensor network where the neuron associated with the hotkey is active. + + Arguments: + hotkey_ss58: The ``SS58`` address of the neuron's hotkey. + block: The blockchain block number for the query. + block_hash: The hash of the blockchain block number at which to perform the query. + reuse_block: Whether to reuse the last-used block hash when retrieving info. + + Returns: + A list of netuids where the neuron is a member. + """ + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + result = await self.substrate.query_map( + module="SubtensorModule", + storage_function="IsNetworkMember", + params=[hotkey_ss58], + block_hash=block_hash, + reuse_block_hash=reuse_block, + ) + netuids = [] + if result.records: + async for record in result: + if record[1].value: + netuids.append(record[0]) + return netuids + + async def get_neuron_certificate( + self, + hotkey: str, + netuid: int, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> Optional[Certificate]: + """ + Retrieves the TLS certificate for a specific neuron identified by its unique identifier (UID) within a + specified subnet (netuid) of the Bittensor network. + + Arguments: + hotkey: The hotkey to query. + netuid: The unique identifier of the subnet. + block: The blockchain block number for the query. + block_hash: The hash of the block to retrieve the parameter from. Do not specify if using block or + reuse_block. + reuse_block: Whether to use the last-used block. Do not set if using block_hash or block. + + Returns: + the certificate of the neuron if found, ``None`` otherwise. + + 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], + ), + ) + try: + if certificate: + return Certificate(certificate) + + except AttributeError: + return None + return None + + async def get_all_neuron_certificates( + self, + netuid: int, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> dict[str, Certificate]: + """ + Retrieves the TLS certificates for neurons within a specified subnet (netuid) of the Bittensor network. + + Arguments: + netuid: The unique identifier of the subnet. + block: The blockchain block number for the query. + block_hash: The hash of the block to retrieve the parameter from. Do not specify if using block or + reuse_block. + reuse_block: Whether to use the last-used block. Do not set if using block_hash or block. + + Returns: + {ss58: Certificate} for the key/Certificate pairs on the subnet + + This function is used for certificate discovery for setting up mutual tls communication between neurons. + """ + query_certificates = await self.query_map( + module="SubtensorModule", + name="NeuronCertificates", + params=[netuid], + block=block, + block_hash=block_hash, + reuse_block=reuse_block, + ) + output = {} + async for key, item in query_certificates: + output[decode_account_id(key)] = Certificate(item.value) + return output + + async def get_liquidity_list( + self, + wallet: "Wallet", + netuid: int, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> Optional[list[LiquidityPosition]]: + """ + Retrieves all liquidity positions for the given wallet on a specified subnet (netuid). + Calculates associated fee rewards based on current global and tick-level fee data. + + Arguments: + wallet: Wallet instance to fetch positions for. + netuid: Subnet unique id. + block: The blockchain block number for the query. + block_hash: The hash of the block to retrieve the parameter from. Do not specify if using block or + reuse_block. + reuse_block: Whether to use the last-used block. Do not set if using block_hash or block. + + Returns: + List of liquidity positions, or None if subnet does not exist. + """ + if not await self.subnet_exists(netuid=netuid): + logging.debug(f"Subnet {netuid} does not exist.") + return None + + if not await self.is_subnet_active(netuid=netuid): + logging.debug(f"Subnet {netuid} is not active.") + return None + + block_hash = await self.determine_block_hash( + block=block, block_hash=block_hash, reuse_block=reuse_block + ) + + # Fetch global fees and current price + fee_global_tao_query_sk = await self.substrate.create_storage_key( + pallet="Swap", + storage_function="FeeGlobalTao", + params=[netuid], + block_hash=block_hash, + ) + fee_global_alpha_query_sk = await self.substrate.create_storage_key( + pallet="Swap", + storage_function="FeeGlobalAlpha", + params=[netuid], + block_hash=block_hash, + ) + sqrt_price_query_sk = await self.substrate.create_storage_key( + pallet="Swap", + storage_function="AlphaSqrtPrice", + params=[netuid], + block_hash=block_hash, + ) + ( + (fee_global_tao_query, fee_global_alpha_query, sqrt_price_query), + positions_response, + ) = await asyncio.gather( + self.substrate.query_multi( + [ + fee_global_tao_query_sk, + fee_global_alpha_query_sk, + sqrt_price_query_sk, + ], + block_hash=block_hash, + ), + self.query_map( + module="Swap", + name="Positions", + block=block, + params=[netuid, wallet.coldkeypub.ss58_address], + ), + ) + # convert to floats + 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]) + + # Fetch global fees and current price + current_tick = price_to_tick(sqrt_price**2) + + # Fetch positions + positions_values: list[tuple[dict, 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] + positions_values.append((position, tick_low_idx, tick_high_idx)) + tick_low_sk = await self.substrate.create_storage_key( + pallet="Swap", + storage_function="Ticks", + params=[netuid, tick_low_idx], + block_hash=block_hash, + ) + tick_high_sk = await self.substrate.create_storage_key( + pallet="Swap", + storage_function="Ticks", + params=[netuid, tick_high_idx], + block_hash=block_hash, + ) + positions_storage_keys.extend([tick_low_sk, tick_high_sk]) + + # query all our ticks at once + ticks_query = await self.substrate.query_multi( + positions_storage_keys, block_hash=block_hash + ) + # iterator with just the values + ticks = iter([x[1] for x in ticks_query]) + positions = [] + for position, tick_low_idx, tick_high_idx in positions_values: + tick_low = next(ticks) + tick_high = next(ticks) + # Calculate fees above/below range for both tokens + tao_below = get_fees( + current_tick=current_tick, + tick=tick_low, + tick_index=tick_low_idx, + quote=True, + global_fees_tao=fee_global_tao, + global_fees_alpha=fee_global_alpha, + above=False, + ) + tao_above = get_fees( + current_tick=current_tick, + tick=tick_high, + tick_index=tick_high_idx, + quote=True, + global_fees_tao=fee_global_tao, + global_fees_alpha=fee_global_alpha, + above=True, + ) + alpha_below = get_fees( + current_tick=current_tick, + tick=tick_low, + tick_index=tick_low_idx, + quote=False, + global_fees_tao=fee_global_tao, + global_fees_alpha=fee_global_alpha, + above=False, + ) + alpha_above = get_fees( + current_tick=current_tick, + tick=tick_high, + tick_index=tick_high_idx, + quote=False, + global_fees_tao=fee_global_tao, + global_fees_alpha=fee_global_alpha, + above=True, + ) + + # Calculate fees earned by position + fees_tao, fees_alpha = calculate_fees( + position=position, + global_fees_tao=fee_global_tao, + global_fees_alpha=fee_global_alpha, + tao_fees_below_low=tao_below, + tao_fees_above_high=tao_above, + alpha_fees_below_low=alpha_below, + alpha_fees_above_high=alpha_above, + netuid=netuid, + ) + + 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"), + } + ) + ) + + return positions + + async def get_neuron_for_pubkey_and_subnet( + self, + hotkey_ss58: str, + netuid: int, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> "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 + network. + + Arguments: + hotkey_ss58: The ``SS58`` address of the neuron's hotkey. + netuid: The unique identifier of the subnet. + block: The blockchain block number for the query. + block_hash: The blockchain block number at which to perform the query. + reuse_block: Whether to reuse the last-used blockchain block hash. + + Returns: + Optional[bittensor.core.chain_data.neuron_info.NeuronInfo]: Detailed information about the neuron if found, + ``None`` otherwise. + + This function is crucial for accessing specific neuron data and understanding its status, stake, and other + attributes within a particular subnet of the Bittensor ecosystem. + """ + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + uid_query = await self.substrate.query( + module="SubtensorModule", + 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: + return NeuronInfo.get_null_neuron() + else: + return await self.neuron_for_uid( + uid=uid, + netuid=netuid, + block=block, + block_hash=block_hash, + reuse_block=reuse_block, + ) + + async def get_next_epoch_start_block( + self, + netuid: int, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> Optional[int]: + """ + Calculates the first block number of the next epoch for the given subnet. + + If ``block`` is not provided, the current chain block will be used. Epochs are determined based on the subnet's + tempo (i.e., blocks per epoch). The result is the block number at which the next epoch will begin. + + Arguments: + netuid: The unique identifier of the subnet. + block: The reference block to calculate from. If None, uses the current chain block height. + block_hash: The blockchain block number at which to perform the query. + reuse_block: Whether to reuse the last-used blockchain block hash. + + Returns: + int: The block number at which the next epoch will start. + + Notes: + See also: + """ + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + blocks_since_last_step = await self.blocks_since_last_step( + netuid=netuid, block=block, block_hash=block_hash, reuse_block=reuse_block + ) + tempo = await self.tempo( + netuid=netuid, block=block, block_hash=block_hash, reuse_block=reuse_block + ) + + block = block or await self.substrate.get_block_number(block_hash=block_hash) + if block and blocks_since_last_step is not None and tempo: + return block - blocks_since_last_step + tempo + 1 + return None + + async def get_owned_hotkeys( + self, + coldkey_ss58: str, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> list[str]: + """ + Retrieves all hotkeys owned by a specific coldkey address. + + Arguments: + coldkey_ss58: The SS58 address of the coldkey to query. + block: The blockchain block number for the query. + block_hash: The hash of the blockchain block number for the query. + reuse_block: Whether to reuse the last-used blockchain block hash. + + Returns: + list[str]: A list of hotkey SS58 addresses owned by the coldkey. + """ + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + owned_hotkeys = await self.substrate.query( + module="SubtensorModule", + storage_function="OwnedHotkeys", + params=[coldkey_ss58], + block_hash=block_hash, + reuse_block_hash=reuse_block, + ) + + return [decode_account_id(hotkey[0]) for hotkey in owned_hotkeys or []] + + async def get_stake( + self, + coldkey_ss58: str, + hotkey_ss58: str, + netuid: int, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> Balance: + """ + Returns the stake under a coldkey - hotkey pairing. + + Arguments: + hotkey_ss58: The SS58 address of the hotkey. + coldkey_ss58: The SS58 address of the coldkey. + netuid: The subnet ID. + block: The block number at which to query the stake information. + block_hash: The hash of the block to retrieve the stake from. Do not specify if using block + or reuse_block + reuse_block: Whether to use the last-used block. Do not set if using block_hash or block. + + Returns: + Balance: The stake under the coldkey - hotkey pairing. + """ + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + + alpha_shares = await self.query_subtensor( + name="Alpha", + block=block, + block_hash=block_hash, + reuse_block=reuse_block, + params=[hotkey_ss58, coldkey_ss58, netuid], + ) + hotkey_alpha_result = await self.query_subtensor( + name="TotalHotkeyAlpha", + block=block, + block_hash=block_hash, + reuse_block=reuse_block, + params=[hotkey_ss58, netuid], + ) + hotkey_shares = await self.query_subtensor( + name="TotalHotkeyShares", + block=block, + block_hash=block_hash, + reuse_block=reuse_block, + params=[hotkey_ss58, netuid], + ) + + hotkey_alpha: int = getattr(hotkey_alpha_result, "value", 0) + alpha_shares_as_float = fixed_to_float(alpha_shares) + hotkey_shares_as_float = fixed_to_float(hotkey_shares) + + if hotkey_shares_as_float == 0: + return Balance.from_rao(0).set_unit(netuid=netuid) + + stake = alpha_shares_as_float / hotkey_shares_as_float * hotkey_alpha + + return Balance.from_rao(int(stake)).set_unit(netuid=netuid) + + # TODO: remove unused parameters in SDK.v10 + async def get_stake_add_fee( + self, + amount: Balance, + netuid: int, + coldkey_ss58: str, + hotkey_ss58: str, + block: Optional[int] = None, + ) -> Balance: + """ + Calculates the fee for adding new stake to a hotkey. + + Arguments: + amount: Amount of stake to add in TAO + netuid: Netuid of subnet + coldkey_ss58: SS58 address of source coldkey + hotkey_ss58: SS58 address of destination hotkey + block: Block number at which to perform the calculation + + Returns: + The calculated stake fee as a Balance object + """ + return await self.get_stake_operations_fee( + amount=amount, netuid=netuid, block=block + ) + + async def get_subnet_info( + self, + netuid: int, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> Optional["SubnetInfo"]: + """ + Retrieves detailed information about subnet within the Bittensor network. + This function provides comprehensive data on subnet, including its characteristics and operational parameters. + + Arguments: + netuid: The unique identifier of the subnet. + block: The blockchain block number for the query. + block_hash: The hash of the block to retrieve the stake from. Do not specify if using block + or reuse_block + reuse_block: Whether to use the last-used block. Do not set if using block_hash or block. + + Returns: + SubnetInfo: A SubnetInfo objects, each containing detailed information about a subnet. + + Gaining insights into the subnet's details assists in understanding the network's composition, the roles of + different subnets, and their unique features. + """ + result = await self.query_runtime_api( + runtime_api="SubnetInfoRuntimeApi", + method="get_subnet_info_v2", + params=[netuid], + block=block, + block_hash=block_hash, + reuse_block=reuse_block, + ) + if not result: + return None + return SubnetInfo.from_dict(result) + + async def get_subnet_price( + self, + netuid: int, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> Balance: + """Gets the current Alpha price in TAO for all subnets. + + Arguments: + netuid: The unique identifier of the subnet. + block: The blockchain block number for the query. + block_hash: The hash of the block to retrieve the stake from. Do not specify if using block + or reuse_block + reuse_block: Whether to use the last-used block. Do not set if using block_hash or block. + + Returns: + The current Alpha price in TAO units for the specified subnet. + """ + # SN0 price is always 1 TAO + if netuid == 0: + return Balance.from_tao(1) + + block_hash = await self.determine_block_hash(block=block) + current_sqrt_price = await self.substrate.query( + module="Swap", + storage_function="AlphaSqrtPrice", + params=[netuid], + block_hash=block_hash, + ) + + current_sqrt_price = fixed_to_float(current_sqrt_price) + current_price = current_sqrt_price * current_sqrt_price + return Balance.from_rao(int(current_price * 1e9)) + + async def get_subnet_prices( + self, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> dict[int, Balance]: + """Gets the current Alpha price in TAO for a specified subnet. + + Arguments: + block: The blockchain block number for the query. + block_hash: The hash of the block to retrieve the stake from. Do not specify if using block + or reuse_block + reuse_block: Whether to use the last-used block. Do not set if using block_hash or block. + + Returns: + dict: + - subnet unique ID + - The current Alpha price in TAO units for the specified subnet. + """ + block_hash = await self.determine_block_hash( + block=block, block_hash=block_hash, reuse_block=reuse_block + ) + + current_sqrt_prices = await self.substrate.query_map( + module="Swap", + storage_function="AlphaSqrtPrice", + block_hash=block_hash, + page_size=129, # total number of subnets + ) + + prices = {} + async for id_, current_sqrt_price in current_sqrt_prices: + current_sqrt_price = fixed_to_float(current_sqrt_price) + current_price = current_sqrt_price * current_sqrt_price + current_price_in_tao = Balance.from_rao(int(current_price * 1e9)) + prices.update({id_: current_price_in_tao}) + + # SN0 price is always 1 TAO + prices.update({0: Balance.from_tao(1)}) + return prices + + async def get_timelocked_weight_commits( + self, + netuid: int, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> list[tuple[str, int, str, int]]: + """ + Retrieves CRv4 weight commit information for a specific subnet. + + Arguments: + netuid (int): The unique identifier of the subnet. + block (Optional[int]): The blockchain block number for the query. Default is ``None``. + block_hash: The hash of the block to retrieve the stake from. Do not specify if using block + or reuse_block + reuse_block: Whether to use the last-used block. Do not set if using block_hash or block. + + Returns: + A list of commit details, where each item contains: + - ss58_address: The address of the committer. + - commit_block: The block number when the commitment was made. + - commit_message: The commit message. + - reveal_round: The round when the commitment was revealed. + + The list may be empty if there are no commits found. + """ + block_hash = await self.determine_block_hash( + block=block, block_hash=block_hash, reuse_block=reuse_block + ) + result = await self.substrate.query_map( + module="SubtensorModule", + storage_function="TimelockedWeightCommits", + params=[netuid], + block_hash=block_hash, + ) + + commits = result.records[0][1] if result.records else [] + return [WeightCommitInfo.from_vec_u8_v2(commit) for commit in commits] + + # TODO: remove unused parameters in SDK.v10 + async def get_unstake_fee( + self, + amount: Balance, + netuid: int, + coldkey_ss58: str, + hotkey_ss58: str, + block: Optional[int] = None, + ) -> Balance: + """ + Calculates the fee for unstaking from a hotkey. + + Arguments: + amount: Amount of stake to unstake in TAO + netuid: Netuid of subnet + coldkey_ss58: SS58 address of source coldkey + hotkey_ss58: SS58 address of destination hotkey + block: Block number at which to perform the calculation + + Returns: + The calculated stake fee as a Balance object + """ + return await self.get_stake_operations_fee( + amount=amount, netuid=netuid, block=block + ) + + # TODO: remove unused parameters in SDK.v10 + async def get_stake_movement_fee( + self, + amount: Balance, + origin_netuid: int, + origin_hotkey_ss58: str, + origin_coldkey_ss58: str, + destination_netuid: int, + destination_hotkey_ss58: str, + destination_coldkey_ss58: str, + block: Optional[int] = None, + ) -> Balance: + """ + Calculates the fee for moving stake between hotkeys/subnets/coldkeys. + + Arguments: + amount: Amount of stake to move in TAO + origin_netuid: Netuid of source subnet + origin_hotkey_ss58: SS58 address of source hotkey + origin_coldkey_ss58: SS58 address of source coldkey + destination_netuid: Netuid of destination subnet + destination_hotkey_ss58: SS58 address of destination hotkey + destination_coldkey_ss58: SS58 address of destination coldkey + block: Block number at which to perform the calculation + + Returns: + The calculated stake fee as a Balance object + """ + return await self.get_stake_operations_fee( + amount=amount, netuid=origin_netuid, block=block + ) + + async def get_stake_for_coldkey_and_hotkey( + self, + coldkey_ss58: str, + hotkey_ss58: str, + netuids: Optional[list[int]] = None, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> dict[int, StakeInfo]: + """ + Retrieves all coldkey-hotkey pairing stake across specified (or all) subnets + + Arguments: + coldkey_ss58: The SS58 address of the coldkey. + hotkey_ss58: The SS58 address of the hotkey. + netuids: The subnet IDs to query for. Set to ``None`` for all subnets. + block: The block number at which to query the stake information. + block_hash: The hash of the block to retrieve the stake from. Do not specify if using block + or reuse_block + reuse_block: Whether to use the last-used block. Do not set if using block_hash or block. + + Returns: + A {netuid: StakeInfo} pairing of all stakes across all subnets. + """ + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + if not block_hash and reuse_block: + block_hash = self.substrate.last_block_hash + elif not block_hash: + block_hash = await self.substrate.get_chain_head() + if netuids is None: + all_netuids = await self.get_subnets(block_hash=block_hash) + else: + all_netuids = netuids + results = await asyncio.gather( + *[ + self.query_runtime_api( + "StakeInfoRuntimeApi", + "get_stake_info_for_hotkey_coldkey_netuid", + params=[hotkey_ss58, coldkey_ss58, netuid], + block_hash=block_hash, + ) + for netuid in all_netuids + ] + ) + return { + netuid: StakeInfo.from_dict(result) + for (netuid, result) in zip(all_netuids, results) + } + + async def get_stake_for_coldkey( + self, + coldkey_ss58: str, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> Optional[list["StakeInfo"]]: + """ + Retrieves the stake information for a given coldkey. + + Arguments: + 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: + An optional list of StakeInfo objects, or ``None`` if no stake information is found. + """ + result = await self.query_runtime_api( + runtime_api="StakeInfoRuntimeApi", + method="get_stake_info_for_coldkey", + params=[coldkey_ss58], + block=block, + block_hash=block_hash, + reuse_block=reuse_block, + ) + + if result is None: + return [] + + stakes = StakeInfo.list_from_dicts(result) # type: ignore + return [stake for stake in stakes if stake.stake > 0] + + get_stake_info_for_coldkey = get_stake_for_coldkey + + async def get_stake_for_hotkey( + self, + hotkey_ss58: str, + netuid: int, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> Balance: + """ + Retrieves the stake information for a given hotkey. + + Arguments: + hotkey_ss58: The SS58 address of the hotkey. + netuid: The subnet ID to query for. + block: The block number at which to query the stake information. Do not specify if also specifying + block_hash or reuse_block. + block_hash: The hash of the blockchain block number for the query. Do not specify if also specifying block + or reuse_block. + reuse_block: Whether to reuse for this query the last-used block. Do not specify if also specifying block + or block_hash. + """ + hotkey_alpha_query = await self.query_subtensor( + name="TotalHotkeyAlpha", + params=[hotkey_ss58, netuid], + block=block, + block_hash=block_hash, + reuse_block=reuse_block, + ) + balance = Balance.from_rao(hotkey_alpha_query.value) + balance.set_unit(netuid=netuid) + return balance + + get_hotkey_stake = get_stake_for_hotkey + + async def get_stake_operations_fee( + self, + amount: Balance, + netuid: int, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ): + """Returns fee for any stake operation in specified subnet. + + Args: + amount: Amount of stake to add in Alpha/TAO. + netuid: Netuid of subnet. + block: The block number at which to query the stake information. Do not specify if also specifying + block_hash or reuse_block. + block_hash: The hash of the blockchain block number for the query. Do not specify if also specifying block + or reuse_block. + reuse_block: Whether to reuse for this query the last-used block. Do not specify if also specifying block + or block_hash. + + Returns: + The calculated stake fee as a Balance object. + """ + block_hash = await self.determine_block_hash( + block=block, block_hash=block_hash, reuse_block=reuse_block + ) + result = await self.substrate.query( + module="Swap", + storage_function="FeeRate", + params=[netuid], + block_hash=block_hash, + ) + return amount * (result.value / U16_MAX) + + async def get_stake_weight( + self, + netuid: int, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> list[float]: + """ + Retrieves the stake weight for all hotkeys in a given subnet. + + Arguments: + netuid: Netuid of subnet. + block: Block number at which to perform the calculation. + block_hash: The hash of the blockchain block number for the query. Do not specify if also specifying block + or reuse_block. + reuse_block: Whether to reuse for this query the last-used block. Do not specify if also specifying block + or block_hash. + + Returns: + A list of stake weights for all hotkeys in the specified subnet. + """ + 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="StakeWeight", + params=[netuid], + block_hash=block_hash, + ) + return [u16_normalized_float(w) for w in result] + + async def get_subnet_burn_cost( + self, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> Optional[Balance]: + """ + Retrieves the burn cost for registering a new subnet within the Bittensor network. This cost represents the + amount of Tao that needs to be locked or burned to establish a new + + Arguments: + block: The blockchain block number for the query. + block_hash: The blockchain block_hash of the block id. + reuse_block: Whether to reuse the last-used block hash. + + Returns: + int: The burn cost for subnet registration. + + The subnet burn cost is an important economic parameter, reflecting the network's mechanisms for controlling + the proliferation of subnets and ensuring their commitment to the network's long-term viability. + """ + lock_cost = await self.query_runtime_api( + runtime_api="SubnetRegistrationRuntimeApi", + method="get_network_registration_cost", + params=[], + block=block, + block_hash=block_hash, + reuse_block=reuse_block, + ) + if lock_cost is not None: + return Balance.from_rao(lock_cost) + else: + return lock_cost + + async def get_subnet_hyperparameters( + self, + netuid: int, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> Optional["SubnetHyperparameters"]: + """ + Retrieves the hyperparameters for a specific subnet within the Bittensor network. These hyperparameters define + the operational settings and rules governing the subnet's behavior. + + Arguments: + netuid: The network UID of the subnet to query. + block: The blockchain block number for the query. + block_hash: The hash of the blockchain block number for the query. + reuse_block: Whether to reuse the last-used blockchain hash. + + Returns: + The subnet's hyperparameters, or ``None`` if not available. + + Understanding the hyperparameters is crucial for comprehending how subnets are configured and managed, and how + they interact with the network's consensus and incentive mechanisms. + """ + result = await self.query_runtime_api( + runtime_api="SubnetInfoRuntimeApi", + method="get_subnet_hyperparams_v2", + params=[netuid], + block=block, + block_hash=block_hash, + reuse_block=reuse_block, + ) + + if not result: + return None + + return SubnetHyperparameters.from_dict(result) + + async def get_subnet_reveal_period_epochs( + self, netuid: int, block: Optional[int] = None, block_hash: Optional[str] = None + ) -> int: + """Retrieve the SubnetRevealPeriodEpochs hyperparameter.""" + block_hash = await self.determine_block_hash(block, block_hash) + return await self.get_hyperparameter( + param_name="RevealPeriodEpochs", block_hash=block_hash, netuid=netuid + ) + + async def get_subnets( + self, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> list[int]: + """ + Retrieves the list of all subnet unique identifiers (netuids) currently present in the Bittensor network. + + Arguments: + block: The blockchain block number for the query. + block_hash: The hash of the block to retrieve the subnet unique identifiers from. + reuse_block: Whether to reuse the last-used block hash. + + Returns: + A list of subnet netuids. + + This function provides a comprehensive view of the subnets within the Bittensor network, offering insights into + its diversity and scale. + """ + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + result = await self.substrate.query_map( + module="SubtensorModule", + storage_function="NetworksAdded", + block_hash=block_hash, + reuse_block_hash=reuse_block, + ) + subnets = [] + if result.records: + async for netuid, exists in result: + if exists: + subnets.append(netuid) + return subnets + + async def get_total_subnets( + self, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> Optional[int]: + """ + Retrieves the total number of subnets within the Bittensor network as of a specific blockchain block. + + Arguments: + block: The blockchain block number for the query. + block_hash: The blockchain block_hash representation of block id. + reuse_block: Whether to reuse the last-used block hash. + + Returns: + Optional[str]: The total number of subnets in the network. + + Understanding the total number of subnets is essential for assessing the network's growth and the extent of its + decentralized infrastructure. + """ + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + result = await self.substrate.query( + module="SubtensorModule", + storage_function="TotalNetworks", + params=[], + block_hash=block_hash, + reuse_block_hash=reuse_block, + ) + return getattr(result, "value", None) + + async def get_transfer_fee( + self, wallet: "Wallet", dest: str, value: Balance, keep_alive: bool = True + ) -> Balance: + """ + Calculates the transaction fee for transferring tokens from a wallet to a specified destination address. This + function simulates the transfer to estimate the associated cost, taking into account the current network + conditions and transaction complexity. + + Arguments: + wallet: The wallet from which the transfer is initiated. + dest: The ``SS58`` address of the destination account. + value: The amount of tokens to be transferred, specified as a Balance object, or in Tao (float) or Rao + (int) units. + keep_alive: Whether the transfer fee should be calculated based on keeping the wallet alive (existential + deposit) or not. + + Returns: + bittensor.utils.balance.Balance: The estimated transaction fee for the transfer, represented as a Balance + object. + + Estimating the transfer fee is essential for planning and executing token transactions, ensuring that the + wallet has sufficient funds to cover both the transfer amount and the associated costs. This function provides + a crucial tool for managing financial operations within the Bittensor network. + """ + if value is not None: + value = check_and_convert_to_balance(value) + call_params: dict[str, Union[int, str, bool]] + call_function, call_params = get_transfer_fn_params(value, dest, keep_alive) + + call = await self.substrate.compose_call( + call_module="Balances", + call_function=call_function, + call_params=call_params, + ) + + try: + payment_info = await self.substrate.get_payment_info( + call=call, keypair=wallet.coldkeypub + ) + except Exception as e: + logging.error(f":cross_mark: [red]Failed to get payment info: [/red]{e}") + payment_info = {"partial_fee": int(2e7)} # assume 0.02 Tao + + return Balance.from_rao(payment_info["partial_fee"]) + + async def get_vote_data( + self, + proposal_hash: str, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> Optional["ProposalVoteData"]: + """ + Retrieves the voting data for a specific proposal on the Bittensor blockchain. This data includes information + about how senate members have voted on the proposal. + + Arguments: + proposal_hash: The hash of the proposal for which voting data is requested. + block: The blockchain block number for the query. + block_hash: The hash of the blockchain block number to query the voting data. + reuse_block: Whether to reuse the last-used blockchain block hash. + + Returns: + An object containing the proposal's voting data, or ``None`` if not found. + + 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. + """ + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + vote_data: dict[str, Any] = await self.substrate.query( + module="Triumvirate", + storage_function="Voting", + params=[proposal_hash], + block_hash=block_hash, + reuse_block_hash=reuse_block, + ) + + if vote_data is None: + return None + + return ProposalVoteData.from_dict(vote_data) + + async def get_uid_for_hotkey_on_subnet( + self, + hotkey_ss58: str, + netuid: int, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> Optional[int]: + """ + Retrieves the unique identifier (UID) for a neuron's hotkey on a specific subnet. + + Arguments: + hotkey_ss58: The ``SS58`` address of the neuron's hotkey. + netuid: The unique identifier of the subnet. + block: The blockchain block number for the query. + block_hash: The blockchain block_hash representation of the block id. + reuse_block: Whether to reuse the last-used blockchain block hash. + + Returns: + Optional[int]: The UID of the neuron if it is registered on the subnet, ``None`` otherwise. + + The UID is a critical identifier within the network, linking the neuron's hotkey to its operational and + governance activities on a particular subnet. + """ + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + result = await self.substrate.query( + module="SubtensorModule", + storage_function="Uids", + params=[netuid, hotkey_ss58], + block_hash=block_hash, + reuse_block_hash=reuse_block, + ) + return getattr(result, "value", result) + + async def filter_netuids_by_registered_hotkeys( + self, + all_netuids: Iterable[int], + filter_for_netuids: Iterable[int], + all_hotkeys: Iterable["Wallet"], + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> list[int]: + """ + Filters a given list of all netuids for certain specified netuids and hotkeys + + Arguments: + all_netuids: A list of netuids to filter. + filter_for_netuids: A subset of all_netuids to filter from the main list. + all_hotkeys: Hotkeys to filter from the main list. + block: The blockchain block number for the query. + block_hash: hash of the blockchain block number at which to perform the query. + reuse_block: whether to reuse the last-used blockchain hash when retrieving info. + + Returns: + The filtered list of netuids. + """ + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + netuids_with_registered_hotkeys = [ + item + for sublist in await asyncio.gather( + *[ + self.get_netuids_for_hotkey( + wallet.hotkey.ss58_address, + reuse_block=reuse_block, + block_hash=block_hash, + ) + for wallet in all_hotkeys + ] + ) + for item in sublist + ] + + if not filter_for_netuids: + all_netuids = netuids_with_registered_hotkeys + + else: + filtered_netuids = [ + netuid for netuid in all_netuids if netuid in filter_for_netuids + ] + + registered_hotkeys_filtered = [ + netuid + for netuid in netuids_with_registered_hotkeys + if netuid in filter_for_netuids + ] + + # Combine both filtered lists + all_netuids = filtered_netuids + registered_hotkeys_filtered + + return list(set(all_netuids)) + + async def immunity_period( + self, + netuid: int, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> Optional[int]: + """ + Retrieves the 'ImmunityPeriod' hyperparameter for a specific subnet. This parameter defines the duration during + which new neurons are protected from certain network penalties or restrictions. + + Arguments: + netuid: The unique identifier of the subnet. + block: The blockchain block number for the query. + block_hash: The blockchain block_hash representation of the block id. + reuse_block: Whether to reuse the last-used blockchain block hash. + + Returns: + Optional[int]: The value of the 'ImmunityPeriod' hyperparameter if the subnet exists, ``None`` otherwise. + + The 'ImmunityPeriod' is a critical aspect of the network's governance system, ensuring that new participants + have a grace period to establish themselves and contribute to the network without facing immediate punitive + actions. + """ + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + call = await self.get_hyperparameter( + param_name="ImmunityPeriod", + netuid=netuid, + block_hash=block_hash, + reuse_block=reuse_block, + ) + return None if call is None else int(call) + + async def is_fast_blocks(self): + """Returns True if the node is running with fast blocks. False if not.""" + return ( + await self.query_constant("SubtensorModule", "DurationOfStartCall") + ).value == 10 + + async def is_hotkey_delegate( + self, + hotkey_ss58: str, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> bool: + """ + Determines whether a given hotkey (public key) is a delegate on the Bittensor network. This function checks if + the neuron associated with the hotkey is part of the network's delegation system. + + Arguments: + hotkey_ss58: The SS58 address of the neuron's hotkey. + block: The blockchain block number for the query. + block_hash: The hash of the blockchain block number for the query. + reuse_block: Whether to reuse the last-used block hash. + + Returns: + ``True`` if the hotkey is a delegate, ``False`` otherwise. + + Being a delegate is a significant status within the Bittensor network, indicating a neuron's involvement in + consensus and governance processes. + """ + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + delegates = await self.get_delegates( + block_hash=block_hash, reuse_block=reuse_block + ) + return hotkey_ss58 in [info.hotkey_ss58 for info in delegates] + + async def is_hotkey_registered( + self, + hotkey_ss58: str, + netuid: Optional[int] = None, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> bool: + """ + Determines whether a given hotkey (public key) is registered in the Bittensor network, either globally across + any subnet or specifically on a specified subnet. This function checks the registration status of a neuron + identified by its hotkey, which is crucial for validating its participation and activities within the network. + + Arguments: + hotkey_ss58: The SS58 address of the neuron's hotkey. + netuid: The unique identifier of the subnet to check the registration. If ``None``, the + registration is checked across all subnets. + block: The blockchain block number at which to perform the query. + block_hash: The blockchain block_hash representation of the block id. Do not specify if using block or + reuse_block. + reuse_block: Whether to reuse the last-used blockchain block hash. Do not set if using block_hash or + reuse_block. + + Returns: + bool: ``True`` if the hotkey is registered in the specified context (either any subnet or a specific subnet), + ``False`` otherwise. + + This function is important for verifying the active status of neurons in the Bittensor network. It aids in + understanding whether a neuron is eligible to participate in network processes such as consensus, validation, + and incentive distribution based on its registration status. + """ + if netuid is None: + return await self.is_hotkey_registered_any( + hotkey_ss58, block, block_hash, reuse_block + ) + else: + return await self.is_hotkey_registered_on_subnet( + hotkey_ss58, netuid, block, block_hash, reuse_block + ) + + async def is_hotkey_registered_any( + self, + hotkey_ss58: str, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> bool: + """ + Checks if a neuron's hotkey is registered on any subnet within the Bittensor network. + + Arguments: + hotkey_ss58: The ``SS58`` address of the neuron's hotkey. + block: The blockchain block number for the query. + block_hash: The blockchain block_hash representation of block id. + reuse_block: Whether to reuse the last-used block hash. + + Returns: + bool: ``True`` if the hotkey is registered on any subnet, False otherwise. + + This function is essential for determining the network-wide presence and participation of a neuron. + """ + hotkeys = await self.get_netuids_for_hotkey( + hotkey_ss58, block, block_hash, reuse_block + ) + return len(hotkeys) > 0 + + async def is_hotkey_registered_on_subnet( + self, + hotkey_ss58: str, + netuid: int, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> bool: + """Checks if the hotkey is registered on a given netuid.""" + return ( + await self.get_uid_for_hotkey_on_subnet( + hotkey_ss58, + netuid, + block=block, + block_hash=block_hash, + reuse_block=reuse_block, + ) + is not None + ) + + async def is_subnet_active( + self, + netuid: int, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> bool: + """Verify if subnet with provided netuid is active. + + Arguments: + netuid: The unique identifier of the subnet. + block: The blockchain block number for the query. + block_hash: The blockchain block_hash representation of block id. + reuse_block: Whether to reuse the last-used block hash. + + Returns: + ``True`` if subnet is active, ``False`` otherwise. + + Note: This means whether the ``start_call`` was initiated or not. + """ + query = await self.query_subtensor( + name="FirstEmissionBlockNumber", + block=block, + block_hash=block_hash, + reuse_block=reuse_block, + params=[netuid], + ) + return True if query and query.value > 0 else False + + async def last_drand_round(self) -> Optional[int]: + """ + Retrieves the last drand round emitted in bittensor. This corresponds when committed weights will be revealed. + + Returns: + int: The latest Drand round emitted in bittensor. + """ + result = await self.substrate.query( + module="Drand", storage_function="LastStoredRound" + ) + return getattr(result, "value", None) + + async def max_weight_limit( + self, + netuid: int, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> Optional[float]: + """ + Returns network MaxWeightsLimit hyperparameter. + + Arguments: + netuid: The unique identifier of the subnetwork. + block: The blockchain block number for the query. + block_hash: The blockchain block_hash representation of block id. + reuse_block: Whether to reuse the last-used block hash. + + Returns: + Optional[float]: The value of the MaxWeightsLimit hyperparameter, or ``None`` if the subnetwork does not + exist or the parameter is not found. + """ + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + call = await self.get_hyperparameter( + param_name="MaxWeightsLimit", + netuid=netuid, + block_hash=block_hash, + reuse_block=reuse_block, + ) + return None if call is None else u16_normalized_float(int(call)) + + async def metagraph( + self, netuid: int, lite: bool = True, block: Optional[int] = None + ) -> "AsyncMetagraph": + """ + Returns a synced metagraph for a specified subnet within the Bittensor network. The metagraph represents the + network's structure, including neuron connections and interactions. + + Arguments: + netuid: The network UID of the subnet to query. + lite: If true, returns a metagraph using a lightweight sync (no weights, no bonds). Default is + ``True``. + block: Block number for synchronization, or `None` for the latest block. + + Returns: + bittensor.core.metagraph.Metagraph: The metagraph representing the subnet's structure and neuron + relationships. + + The metagraph is an essential tool for understanding the topology and dynamics of the Bittensor network's + decentralized architecture, particularly in relation to neuron interconnectivity and consensus processes. + """ + metagraph = AsyncMetagraph( + network=self.chain_endpoint, + netuid=netuid, + lite=lite, + sync=False, + subtensor=self, + ) + await metagraph.sync(block=block, lite=lite, subtensor=self) + + return metagraph + + async def min_allowed_weights( + self, + netuid: int, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> Optional[int]: + """ + Returns network MinAllowedWeights hyperparameter. + + Arguments: + netuid: The unique identifier of the subnetwork. + block: The blockchain block number for the query. + block_hash: The blockchain block_hash representation of block id. + reuse_block: Whether to reuse the last-used block hash. + + Returns: + Optional[int]: The value of the MinAllowedWeights hyperparameter, or ``None`` if the subnetwork does not + exist or the parameter is not found. + """ + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + call = await self.get_hyperparameter( + param_name="MinAllowedWeights", + netuid=netuid, + block_hash=block_hash, + reuse_block=reuse_block, + ) + return None if call is None else int(call) + + async def neuron_for_uid( + self, + uid: Optional[int], + netuid: int, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> NeuronInfo: + """ + Retrieves detailed information about a specific neuron identified by its unique identifier (UID) within a + specified subnet (netuid) of the Bittensor network. This function provides a comprehensive view of a neuron's + attributes, including its stake, rank, and operational status. + + Arguments: + uid: The unique identifier of the neuron. + netuid: The unique identifier of the subnet. + block: The blockchain block number for the query. + block_hash: The hash of the blockchain block number for the query. + reuse_block: Whether to reuse the last-used blockchain block hash. + + Returns: + Detailed information about the neuron if found, a null neuron otherwise + + This function is crucial for analyzing individual neurons' contributions and status within a specific subnet, + offering insights into their roles in the network's consensus and validation mechanisms. + """ + if uid is None: + return NeuronInfo.get_null_neuron() + + result = await self.query_runtime_api( + runtime_api="NeuronInfoRuntimeApi", + method="get_neuron", + params=[netuid, uid], + block=block, + block_hash=block_hash, + reuse_block=reuse_block, + ) + + if not result: + return NeuronInfo.get_null_neuron() + + return NeuronInfo.from_dict(result) + + async def neurons( + self, + netuid: int, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> list[NeuronInfo]: + """ + Retrieves a list of all neurons within a specified subnet of the Bittensor network. + This function provides a snapshot of the subnet's neuron population, including each neuron's attributes and + network interactions. + + Arguments: + netuid: The unique identifier of the subnet. + block: The blockchain block number for the query. + block_hash: The hash of the blockchain block number for the query. + reuse_block: Whether to reuse the last-used blockchain block hash. + + Returns: + A list of NeuronInfo objects detailing each neuron's characteristics in the subnet. + + Understanding the distribution and status of neurons within a subnet is key to comprehending the network's + decentralized structure and the dynamics of its consensus and governance processes. + """ + result = await self.query_runtime_api( + runtime_api="NeuronInfoRuntimeApi", + method="get_neurons", + params=[netuid], + block=block, + block_hash=block_hash, + reuse_block=reuse_block, + ) + + if not result: + return [] + + return NeuronInfo.list_from_dicts(result) + + async def neurons_lite( + self, + netuid: int, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> list[NeuronInfoLite]: + """ + Retrieves a list of neurons in a 'lite' format from a specific subnet of the Bittensor network. + This function provides a streamlined view of the neurons, focusing on key attributes such as stake and network + participation. + + Arguments: + netuid: The unique identifier of the subnet. + block: The blockchain block number for the query. + block_hash: The hash of the blockchain block number for the query. + reuse_block: Whether to reuse the last-used blockchain block hash. + + Returns: + A list of simplified neuron information for the subnet. + + This function offers a quick overview of the neuron population within a subnet, facilitating efficient analysis + of the network's decentralized structure and neuron dynamics. + """ + result = await self.query_runtime_api( + runtime_api="NeuronInfoRuntimeApi", + method="get_neurons_lite", + params=[netuid], + block=block, + block_hash=block_hash, + reuse_block=reuse_block, + ) + + if not result: + return [] + + return NeuronInfoLite.list_from_dicts(result) + + async def query_identity( + self, + coldkey_ss58: str, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> Optional[ChainIdentity]: + """ + Queries the identity of a neuron on the Bittensor blockchain using the given key. This function retrieves + detailed identity information about a specific neuron, which is a crucial aspect of the network's decentralized + identity and governance system. + + Arguments: + coldkey_ss58: The coldkey used to query the neuron's identity (technically the neuron's coldkey SS58 + address). + block: The blockchain block number for the query. + block_hash: The hash of the blockchain block number at which to perform the query. + reuse_block: Whether to reuse the last-used blockchain block hash. + + Returns: + An object containing the identity information of the neuron if found, ``None`` otherwise. + + The identity information can include various attributes such as the neuron's stake, rank, and other + network-specific details, providing insights into the neuron's role and status within the Bittensor network. + + Note: + See the ``Bittensor CLI documentation ``_ for supported identity + parameters. + """ + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + identity_info = cast( + dict, + await self.substrate.query( + module="SubtensorModule", + storage_function="IdentitiesV2", + params=[coldkey_ss58], + block_hash=block_hash, + reuse_block_hash=reuse_block, + ), + ) + + if not identity_info: + return None + + try: + return ChainIdentity.from_dict( + decode_hex_identity_dict(identity_info), + ) + except TypeError: + return None + + async def recycle( + self, + netuid: int, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> Optional[Balance]: + """ + Retrieves the 'Burn' hyperparameter for a specified subnet. The 'Burn' parameter represents the amount of Tao + that is effectively recycled within the Bittensor network. + + Arguments: + netuid: The unique identifier of the subnet. + block: The blockchain block number for the query. + block_hash: The hash of the blockchain block number for the query. + reuse_block: Whether to reuse the last-used blockchain block hash. + + Returns: + Optional[Balance]: The value of the 'Burn' hyperparameter if the subnet exists, ``None`` otherwise. + + Understanding the 'Burn' rate is essential for analyzing the network registration usage, particularly how it is + correlated with user activity and the overall cost of participation in a given subnet. + """ + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + call = await self.get_hyperparameter( + param_name="Burn", + netuid=netuid, + block_hash=block_hash, + reuse_block=reuse_block, + ) + return None if call is None else Balance.from_rao(int(call)) + + async def set_reveal_commitment( + self, + wallet, + netuid: int, + data: str, + blocks_until_reveal: int = 360, + block_time: Union[int, float] = 12, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + ) -> tuple[bool, int]: + """ + Commits arbitrary data to the Bittensor network by publishing metadata. + + Parameters: + wallet: The wallet associated with the neuron committing the data. + netuid: The unique identifier of the subnetwork. + data: The data to be committed to the network. + blocks_until_reveal: The number of blocks from now after which the data will be revealed. Then number of + blocks in one epoch. + block_time: The number of seconds between each block. + 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. + + Returns: + bool: `True` if the commitment was successful, `False` otherwise. + + Note: A commitment can be set once per subnet epoch and is reset at the next epoch in the chain automatically. + """ + + encrypted, reveal_round = get_encrypted_commitment( + data, blocks_until_reveal, block_time + ) + + # increase reveal_round in return + 1 because we want to fetch data from the chain after that round was revealed + # and stored. + data_ = {"encrypted": encrypted, "reveal_round": reveal_round} + return await publish_metadata( + subtensor=self, + wallet=wallet, + netuid=netuid, + data_type="TimelockEncrypted", + data=data_, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ), reveal_round + + async def subnet( + self, + netuid: int, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> Optional[DynamicInfo]: + """ + Retrieves the subnet information for a single subnet in the Bittensor network. + + Arguments: + netuid: The unique identifier of the subnet. + block: The block number to get the subnets at. + block_hash: The hash of the blockchain block number for the query. + reuse_block: Whether to reuse the last-used blockchain block hash. + + Returns: + Optional[DynamicInfo]: A DynamicInfo object, containing detailed information about a subnet. + """ + block_hash = await self.determine_block_hash( + block=block, block_hash=block_hash, reuse_block=reuse_block + ) + + if not block_hash and reuse_block: + block_hash = self.substrate.last_block_hash + + query, 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(price, (SubstrateRequestException, ValueError)): + price = None + return DynamicInfo.from_dict({**decoded, "price": price}) + return None + + async def subnet_exists( + self, + netuid: int, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> bool: + """ + Checks if a subnet with the specified unique identifier (netuid) exists within the Bittensor network. + + Arguments: + netuid: The unique identifier of the subnet. + block: The blockchain block number for the query. + block_hash: The hash of the blockchain block number at which to check the subnet existence. + reuse_block: Whether to reuse the last-used block hash. + + Returns: + ``True`` if the subnet exists, ``False`` otherwise. + + This function is critical for verifying the presence of specific subnets in the network, enabling a deeper + understanding of the network's structure and composition. + """ + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + result = await self.substrate.query( + module="SubtensorModule", + storage_function="NetworksAdded", + params=[netuid], + block_hash=block_hash, + reuse_block_hash=reuse_block, + ) + return getattr(result, "value", False) + + async def subnetwork_n( + self, + netuid: int, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> Optional[int]: + """ + Returns network SubnetworkN hyperparameter. + + Arguments: + netuid: The unique identifier of the subnetwork. + block: The blockchain block number for the query. + block_hash: The hash of the blockchain block number at which to check the subnet existence. + reuse_block: Whether to reuse the last-used block hash. + + Returns: + Optional[int]: The value of the SubnetworkN hyperparameter, or ``None`` if the subnetwork does not exist or + the parameter is not found. + """ + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + call = await self.get_hyperparameter( + param_name="SubnetworkN", + netuid=netuid, + block_hash=block_hash, + reuse_block=reuse_block, + ) + return None if call is None else int(call) + + async def tempo( + self, + netuid: int, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> Optional[int]: + """ + Returns network Tempo hyperparameter. + + Arguments: + netuid: The unique identifier of the subnetwork. + block: The blockchain block number for the query. + block_hash: The hash of the blockchain block number at which to check the subnet existence. + reuse_block: Whether to reuse the last-used block hash. + + Returns: + Optional[int]: The value of the Tempo hyperparameter, or ``None`` if the subnetwork does not exist or the + parameter is not found. + """ + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + call = await self.get_hyperparameter( + param_name="Tempo", + netuid=netuid, + block_hash=block_hash, + reuse_block=reuse_block, + ) + return None if call is None else int(call) + + async def tx_rate_limit( + self, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> Optional[int]: + """ + Retrieves the transaction rate limit for the Bittensor network as of a specific blockchain block. + This rate limit sets the maximum number of transactions that can be processed within a given time frame. + + Arguments: + block: The blockchain block number for the query. + block_hash: The hash of the blockchain block number at which to check the subnet existence. + reuse_block: Whether to reuse the last-used block hash. + + Returns: + Optional[int]: The transaction rate limit of the network, ``None`` if not available. + + The transaction rate limit is an essential parameter for ensuring the stability and scalability of the Bittensor + network. It helps in managing network load and preventing congestion, thereby maintaining efficient and timely + transaction processing. + """ + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + result = await self.query_subtensor( + "TxRateLimit", block_hash=block_hash, reuse_block=reuse_block + ) + return getattr(result, "value", None) + + async def wait_for_block(self, block: Optional[int] = None): + """ + Waits until a specific block is reached on the chain. If no block is specified, waits for the next block. + + Arguments: + block: The block number to wait for. If ``None``, waits for the next block. + + Returns: + bool: ``True`` if the target block was reached, ``False`` if timeout occurred. + + Example: + import bittensor as bt + subtensor = bt.Subtensor() + + await subtensor.wait_for_block() # Waits for next block + await subtensor.wait_for_block(block=1234) # Waits for a specific block + """ + + async def handler(block_data: dict): + logging.debug( + f"reached block {block_data['header']['number']}. Waiting for block {target_block}" + ) + if block_data["header"]["number"] >= target_block: + return True + return None + + current_block = await self.substrate.get_block() + current_block_hash = current_block.get("header", {}).get("hash") + + if block is not None: + target_block = block + else: + target_block = current_block["header"]["number"] + 1 + + await self.substrate.get_block_handler( + current_block_hash, header_only=True, subscription_handler=handler + ) + return True + + async def weights( + self, + netuid: int, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> list[tuple[int, list[tuple[int, int]]]]: + """ + Retrieves the weight distribution set by neurons within a specific subnet of the Bittensor network. + This function maps each neuron's UID to the weights it assigns to other neurons, reflecting the network's trust + and value assignment mechanisms. + + Arguments: + netuid: The network UID of the subnet to query. + block: Block number for synchronization, or `None` for the latest block. + block_hash: The hash of the blockchain block for the query. + reuse_block: reuse the last-used blockchain block hash. + + Returns: + A list of tuples mapping each neuron's UID to its assigned weights. + + The weight distribution is a key factor in the network's consensus algorithm and the ranking of neurons, + influencing their influence and reward allocation within the subnet. + """ + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + # TODO look into seeing if we can speed this up with storage query + w_map_encoded = await self.substrate.query_map( + module="SubtensorModule", + storage_function="Weights", + params=[netuid], + 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)) + + return w_map + + async def weights_rate_limit( + self, + netuid: int, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> Optional[int]: + """ + Returns network WeightsSetRateLimit hyperparameter. + + Arguments: + netuid: The unique identifier of the subnetwork. + block: The blockchain block number for the query. + block_hash: The blockchain block_hash representation of the block id. + reuse_block: Whether to reuse the last-used blockchain block hash. + + Returns: + Optional[int]: The value of the WeightsSetRateLimit hyperparameter, or ``None`` if the subnetwork does not + exist or the parameter is not found. + """ + block_hash = await self.determine_block_hash(block, block_hash, reuse_block) + call = await self.get_hyperparameter( + param_name="WeightsSetRateLimit", + netuid=netuid, + block_hash=block_hash, + reuse_block=reuse_block, + ) + return None if call is None else int(call) + + async def get_timestamp( + self, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> datetime: + """ + Retrieves the datetime timestamp for a given block. + + Arguments: + block: The blockchain block number for the query. Do not specify if specifying block_hash or reuse_block. + block_hash: The blockchain block_hash representation of the block id. Do not specify if specifying block + or reuse_block. + reuse_block: Whether to reuse the last-used blockchain block hash. Do not specify if specifying block or + block_hash. + + Returns: + datetime object for the timestamp of the block. + """ + res = await self.query_module( + "Timestamp", + "Now", + block=block, + block_hash=block_hash, + reuse_block=reuse_block, + ) + unix = res.value + return datetime.fromtimestamp(unix / 1000, tz=timezone.utc) + + async def get_subnet_owner_hotkey( + self, netuid: int, block: Optional[int] = None + ) -> Optional[str]: + """ + Retrieves the hotkey of the subnet owner for a given network UID. + + This function queries the subtensor network to fetch the hotkey of the owner of a subnet specified by its + netuid. If no data is found or the query fails, the function returns None. + + Arguments: + netuid: The network UID of the subnet to fetch the owner's hotkey for. + block: The specific block number to query the data from. + + Returns: + The hotkey of the subnet owner if available; None otherwise. + """ + return await self.query_subtensor( + name="SubnetOwnerHotkey", params=[netuid], block=block + ) + + async def get_subnet_validator_permits( + self, netuid: int, block: Optional[int] = None + ) -> Optional[list[bool]]: + """ + Retrieves the list of validator permits for a given subnet as boolean values. + + Arguments: + netuid: The unique identifier of the subnetwork. + block: The blockchain block number for the query. + + Returns: + A list of boolean values representing validator permits, or None if not available. + """ + query = await self.query_subtensor( + name="ValidatorPermit", + params=[netuid], + block=block, + ) + return query.value if query is not None and hasattr(query, "value") else query + + # Extrinsics helper ================================================================================================ + + async def sign_and_send_extrinsic( + self, + call: "GenericCall", + wallet: "Wallet", + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, + sign_with: str = "coldkey", + use_nonce: bool = False, + period: Optional[int] = None, + nonce_key: str = "hotkey", + raise_error: bool = False, + ) -> tuple[bool, str]: + """ + Helper method to sign and submit an extrinsic call to chain. + + Arguments: + call: a prepared Call object + wallet: the wallet whose coldkey will be used to sign the extrinsic + wait_for_inclusion: whether to wait until the extrinsic call is included on the chain + wait_for_finalization: whether to wait until the extrinsic call is finalized on the chain + sign_with: the wallet's keypair to use for the signing. Options are "coldkey", "hotkey", "coldkeypub" + use_nonce: unique identifier for the transaction related with hot/coldkey. + 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. + nonce_key: the type on nonce to use. Options are "hotkey" or "coldkey". + nonce_key: the type on nonce to use. Options are "hotkey", "coldkey", or "coldkeypub". + raise_error: raises a relevant exception rather than returning ``False`` if unsuccessful. + + Returns: + (success, error message) + + Raises: + SubstrateRequestException: Substrate request exception. + """ + possible_keys = ("coldkey", "hotkey", "coldkeypub") + if sign_with not in possible_keys: + raise AttributeError( + f"'sign_with' must be either 'coldkey', 'hotkey' or 'coldkeypub', not '{sign_with}'" + ) + signing_keypair = getattr(wallet, sign_with) + extrinsic_data = {"call": call, "keypair": signing_keypair} + if use_nonce: + if nonce_key not in possible_keys: + raise AttributeError( + f"'nonce_key' must be either 'coldkey', 'hotkey' or 'coldkeypub', not '{nonce_key}'" + ) + next_nonce = await self.substrate.get_account_next_index( + getattr(wallet, nonce_key).ss58_address + ) + extrinsic_data["nonce"] = next_nonce + if period is not None: + extrinsic_data["era"] = {"period": period} + + extrinsic = await self.substrate.create_signed_extrinsic(**extrinsic_data) + try: + response = await self.substrate.submit_extrinsic( + extrinsic, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + # We only wait here if we expect finalization. + if not wait_for_finalization and not wait_for_inclusion: + message = "Not waiting for finalization or inclusion." + logging.debug(f"{message}. Extrinsic: {extrinsic}") + return True, message + + if await response.is_success: + return True, "" + + if raise_error: + raise ChainError.from_error(await response.error_message) + + return False, format_error_message(await response.error_message) + + except SubstrateRequestException as e: + if raise_error: + raise + + return False, format_error_message(e) + + # Extrinsics ======================================================================================================= + + async def add_stake( + self, + wallet: "Wallet", + netuid: int, + hotkey_ss58: str, + amount: Balance, + safe_staking: bool = False, + allow_partial_stake: bool = False, + rate_tolerance: float = 0.005, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + ) -> bool: + """ + Adds a stake from the specified wallet to the neuron identified by the SS58 address of its hotkey in specified + subnet. Staking is a fundamental process in the Bittensor network that enables neurons to participate actively + and earn incentives. + + Parameters: + wallet: The wallet to be used for staking. + netuid: The unique identifier of the subnet to which the neuron belongs. + hotkey_ss58: The `ss58` address of the hotkey account to stake to default to the wallet's hotkey. + amount: The amount of TAO to stake. + safe_staking: If true, enables price safety checks to protect against fluctuating prices. The stake will + only execute if the price change doesn't exceed the rate tolerance. Default is ``False``. + allow_partial_stake: If true and safe_staking is enabled, allows partial staking when the full amount would + exceed the price tolerance. If false, the entire stake fails if it would exceed the tolerance. + Default is ``False``. + rate_tolerance: The maximum allowed price change ratio when staking. For example, 0.005 = 0.5% maximum price + increase. Only used when safe_staking is True. + 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 extrinsic to be included in a block. + wait_for_finalization: Whether to wait for finalization of the extrinsic. + + Returns: + bool: ``True`` if the staking is successful, ``False`` otherwise. + + This function enables neurons to increase their stake in the network, enhancing their influence and potential. + When safe_staking is enabled, it provides protection against price fluctuations during the time stake is + executed and the time it is actually processed by the chain. + """ + amount = check_and_convert_to_balance(amount) + return await add_stake_extrinsic( + subtensor=self, + wallet=wallet, + hotkey_ss58=hotkey_ss58, + netuid=netuid, + amount=amount, + safe_staking=safe_staking, + allow_partial_stake=allow_partial_stake, + rate_tolerance=rate_tolerance, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + async def add_liquidity( + self, + wallet: "Wallet", + netuid: int, + liquidity: Balance, + price_low: Balance, + price_high: Balance, + hotkey: Optional[str] = None, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + ) -> tuple[bool, str]: + """ + Adds liquidity to the specified price range. + + Parameters: + wallet: The wallet used to sign the extrinsic (must be unlocked). + netuid: The UID of the target subnet for which the call is being initiated. + liquidity: The amount of liquidity to be added. + price_low: The lower bound of the price tick range. In TAO. + price_high: The upper bound of the price tick range. In TAO. + hotkey: The hotkey with staked TAO in Alpha. If not passed then the wallet hotkey is used. + 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 extrinsic to be included in a block. + wait_for_finalization: Whether to wait for finalization of the extrinsic. + + Returns: + Tuple[bool, str]: + - True and a success message if the extrinsic is successfully submitted or processed. + - False and an error message if the submission fails or the wallet cannot be unlocked. + + Note: Adding is allowed even when user liquidity is enabled in specified subnet. Call ``toggle_user_liquidity`` + method to enable/disable user liquidity. + """ + return await add_liquidity_extrinsic( + subtensor=self, + wallet=wallet, + netuid=netuid, + liquidity=liquidity, + price_low=price_low, + price_high=price_high, + hotkey=hotkey, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + async def add_stake_multiple( + self, + wallet: "Wallet", + hotkey_ss58s: list[str], + netuids: list[int], + amounts: list[Balance], + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + ) -> bool: + """ + Adds stakes to multiple neurons identified by their hotkey SS58 addresses. + This bulk operation allows for efficient staking across different neurons from a single wallet. + + Parameters: + wallet: The wallet used for staking. + hotkey_ss58s: List of ``SS58`` addresses of hotkeys to stake to. + netuids: List of subnet UIDs. + amounts: List of corresponding TAO amounts to bet for each netuid and hotkey. + 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. + + Returns: + bool: ``True`` if the staking is successful for all specified neurons, ``False`` otherwise. + + This function is essential for managing stakes across multiple neurons, reflecting the dynamic and collaborative + nature of the Bittensor network. + """ + return await add_stake_multiple_extrinsic( + subtensor=self, + wallet=wallet, + netuids=netuids, + hotkey_ss58s=hotkey_ss58s, + amounts=amounts, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + async def burned_register( + self, + wallet: "Wallet", + netuid: int, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + ) -> bool: + """ + Registers a neuron on the Bittensor network by recycling TAO. This method of registration involves recycling + TAO tokens, allowing them to be re-mined by performing work on the network. + + Parameters: + wallet: The wallet associated with the neuron to be registered. + netuid: The unique identifier of the subnet. + 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. + + Returns: + bool: ``True`` if the registration is successful, False otherwise. + """ + async with self: + if netuid == 0: + return await root_register_extrinsic( + subtensor=self, + wallet=wallet, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + return await burned_register_extrinsic( + subtensor=self, + wallet=wallet, + netuid=netuid, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + async def commit_weights( + self, + wallet: "Wallet", + netuid: int, + salt: list[int], + uids: Union[NDArray[np.int64], list], + weights: Union[NDArray[np.int64], list], + version_key: int = version_as_int, + max_retries: int = 5, + period: Optional[int] = 16, + raise_error: bool = True, + wait_for_inclusion: bool = False, + wait_for_finalization: bool = False, + ) -> tuple[bool, str]: + """ + Commits a hash of the subnet validator's weight vector to the Bittensor blockchain using the provided wallet. + This action serves as a commitment or snapshot of the validator's current weight distribution. + + Parameters: + wallet: The wallet associated with the neuron committing the weights. + netuid: The unique identifier of the subnet. + salt: list of randomly generated integers as salt to generated weighted hash. + uids: NumPy array of neuron UIDs for which weights are being committed. + weights: NumPy array of weight values corresponding to each UID. + version_key: Version key for compatibility with the network. + max_retries: The number of maximum attempts to commit weights. + 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 extrinsic to be included in a block. + wait_for_finalization: Whether to wait for finalization of the extrinsic. + + Returns: + tuple[bool, str]: + `True` if the weight commitment is successful, False otherwise. + `msg` is a string value describing the success or potential error. + + This function allows subnet validators to create a tamper-proof record of their weight vector at a specific + point in time, creating a foundation of transparency and accountability for the Bittensor network. + + Notes: + See also: , + """ + retries = 0 + success = False + message = "No attempt made. Perhaps it is too soon to commit weights!" + + logging.info( + f"Committing weights with params: " + f"netuid=[blue]{netuid}[/blue], uids=[blue]{uids}[/blue], weights=[blue]{weights}[/blue], " + f"version_key=[blue]{version_key}[/blue]" + ) + + # Generate the hash of the weights + commit_hash = generate_weight_hash( + address=wallet.hotkey.ss58_address, + netuid=netuid, + uids=list(uids), + values=list(weights), + salt=salt, + version_key=version_key, + ) + + while retries < max_retries and success is False: + try: + success, message = await commit_weights_extrinsic( + subtensor=self, + wallet=wallet, + netuid=netuid, + commit_hash=commit_hash, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + period=period, + raise_error=raise_error, + ) + if success: + break + except Exception as e: + logging.error(f"Error committing weights: {e}") + retries += 1 + + return success, message + + async def modify_liquidity( + self, + wallet: "Wallet", + netuid: int, + position_id: int, + liquidity_delta: Balance, + hotkey: Optional[str] = None, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + ) -> tuple[bool, str]: + """Modifies liquidity in liquidity position by adding or removing liquidity from it. + + Parameters: + wallet: The wallet used to sign the extrinsic (must be unlocked). + netuid: The UID of the target subnet for which the call is being initiated. + position_id: The id of the position record in the pool. + liquidity_delta: The amount of liquidity to be added or removed (add if positive or remove if negative). + hotkey: The hotkey with staked TAO in Alpha. If not passed then the wallet hotkey is used. + 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 extrinsic to be included in a block. + wait_for_finalization: Whether to wait for finalization of the extrinsic. + + Returns: + Tuple[bool, str]: + - True and a success message if the extrinsic is successfully submitted or processed. + - False and an error message if the submission fails or the wallet cannot be unlocked. + + Example: + import bittensor as bt + + subtensor = bt.AsyncSubtensor(network="local") + await subtensor.initialize() + + my_wallet = bt.Wallet() + + # if `liquidity_delta` is negative + my_liquidity_delta = Balance.from_tao(100) * -1 + await subtensor.modify_liquidity( + wallet=my_wallet, + netuid=123, + position_id=2, + liquidity_delta=my_liquidity_delta + ) + + # if `liquidity_delta` is positive + my_liquidity_delta = Balance.from_tao(120) + await subtensor.modify_liquidity( + wallet=my_wallet, + netuid=123, + position_id=2, + liquidity_delta=my_liquidity_delta + ) + + Note: Modifying is allowed even when user liquidity is enabled in specified subnet. Call `toggle_user_liquidity` + to enable/disable user liquidity. + """ + return await modify_liquidity_extrinsic( + subtensor=self, + wallet=wallet, + netuid=netuid, + position_id=position_id, + liquidity_delta=liquidity_delta, + hotkey=hotkey, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + async def move_stake( + self, + wallet: "Wallet", + origin_hotkey_ss58: str, + origin_netuid: int, + destination_hotkey_ss58: str, + destination_netuid: int, + amount: Optional[Balance] = None, + move_all_stake: bool = False, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + ) -> bool: + """ + Moves stake to a different hotkey and/or subnet. + + Arguments: + wallet: The wallet to move stake from. + origin_hotkey_ss58: The SS58 address of the source hotkey. + origin_netuid: The netuid of the source subnet. + destination_hotkey_ss58: The SS58 address of the destination hotkey. + destination_netuid: The netuid of the destination subnet. + amount: Amount of stake to move. + move_all_stake: If true, moves all stake from the source hotkey to the destination hotkey. + 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. + + Returns: + success: True if the stake movement was successful. + """ + amount = check_and_convert_to_balance(amount) + return await move_stake_extrinsic( + subtensor=self, + wallet=wallet, + origin_hotkey_ss58=origin_hotkey_ss58, + origin_netuid=origin_netuid, + destination_hotkey_ss58=destination_hotkey_ss58, + destination_netuid=destination_netuid, + amount=amount, + move_all_stake=move_all_stake, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + 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, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + ): + """ + 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. + + 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. + 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. + + Returns: + bool: ``True`` if the registration is successful, False otherwise. + + This function facilitates the entry of new neurons into the network, supporting the decentralized growth and + scalability of the Bittensor ecosystem. + """ + 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, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + async def register_subnet( + self: "AsyncSubtensor", + wallet: "Wallet", + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + ) -> bool: + """ + Registers a new subnetwork on the Bittensor network. + + Parameters: + wallet: The wallet to be used for subnet registration. + 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 extrinsic to be included in a block. + wait_for_finalization: Whether to wait for finalization of the extrinsic. + + Returns: + bool: True if the subnet registration was successful, False otherwise. + """ + return await register_subnet_extrinsic( + subtensor=self, + wallet=wallet, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + async def remove_liquidity( + self, + wallet: "Wallet", + netuid: int, + position_id: int, + hotkey: Optional[str] = None, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + ) -> tuple[bool, str]: + """Remove liquidity and credit balances back to wallet's hotkey stake. + + Parameters: + wallet: The wallet used to sign the extrinsic (must be unlocked). + netuid: The UID of the target subnet for which the call is being initiated. + position_id: The id of the position record in the pool. + hotkey: The hotkey with staked TAO in Alpha. If not passed then the wallet hotkey is used. + 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 extrinsic to be included in a block. + wait_for_finalization: Whether to wait for finalization of the extrinsic. + + Returns: + Tuple[bool, str]: + - True and a success message if the extrinsic is successfully submitted or processed. + - False and an error message if the submission fails or the wallet cannot be unlocked. + + Note: + - Adding is allowed even when user liquidity is enabled in specified subnet. Call `toggle_user_liquidity` + extrinsic to enable/disable user liquidity. + - To get the `position_id` use `get_liquidity_list` method. + """ + return await remove_liquidity_extrinsic( + subtensor=self, + wallet=wallet, + netuid=netuid, + position_id=position_id, + hotkey=hotkey, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + async def reveal_weights( + self, + wallet: "Wallet", + netuid: int, + uids: Union[NDArray[np.int64], list], + weights: Union[NDArray[np.int64], list], + salt: Union[NDArray[np.int64], list], + max_retries: int = 5, + version_key: int = version_as_int, + period: Optional[int] = 16, + raise_error: bool = True, + wait_for_inclusion: bool = False, + wait_for_finalization: bool = False, + ) -> tuple[bool, str]: + """ + Reveals the weight vector for a specific subnet on the Bittensor blockchain using the provided wallet. + This action serves as a revelation of the subnet validator's previously committed weight distribution as part + of the commit-reveal mechanism. + + Parameters: + wallet: The wallet associated with the subnet validator revealing the weights. + netuid: unique identifier of the subnet. + uids: NumPy array of subnet miner neuron UIDs for which weights are being revealed. + weights: NumPy array of weight values corresponding to each UID. + salt: NumPy array of salt values + max_retries: The number of maximum attempts to reveal weights. + version_key: Version key for compatibility with the network. + 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 extrinsic to be included in a block. + wait_for_finalization: Whether to wait for finalization of the extrinsic. + + Returns: + Tuple[bool, str]: + - True and a success message if the extrinsic is successfully submitted or processed. + - False and an error message if the submission fails or the wallet cannot be unlocked. + + This function allows subnet validators to reveal their previously committed weight vector. + + See also: , + """ + retries = 0 + success = False + message = "No attempt made. Perhaps it is too soon to reveal weights!" + + while retries < max_retries and success is False: + try: + success, message = await reveal_weights_extrinsic( + subtensor=self, + wallet=wallet, + netuid=netuid, + uids=list(uids), + weights=list(weights), + salt=list(salt), + version_key=version_key, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + if success: + break + except Exception as e: + logging.error(f"Error revealing weights: {e}") + retries += 1 + + return success, message + + async def root_set_pending_childkey_cooldown( + self, + wallet: "Wallet", + cooldown: int, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + ) -> tuple[bool, str]: + """Sets the pending childkey cooldown. + + Parameters: + wallet: bittensor wallet instance. + cooldown: the number of blocks to setting pending childkey cooldown. + 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. + + Returns: + Tuple[bool, str]: + - True and a success message if the extrinsic is successfully submitted or processed. + - False and an error message if the submission fails or the wallet cannot be unlocked. + + Note: This operation can only be successfully performed if your wallet has root privileges. + """ + return await root_set_pending_childkey_cooldown_extrinsic( + subtensor=self, + wallet=wallet, + cooldown=cooldown, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + # TODO: remove `block_hash` argument + async def root_register( + self, + wallet: "Wallet", + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + ) -> bool: + """ + Register neuron by recycling some TAO. + + Parameters: + wallet: The wallet associated with the neuron to be registered. + 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. + + Returns: + bool: ``True`` if the registration is successful, False otherwise. + """ + + return await root_register_extrinsic( + subtensor=self, + wallet=wallet, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + async def set_children( + self, + wallet: "Wallet", + hotkey: str, + netuid: int, + children: list[tuple[float, str]], + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + ) -> tuple[bool, str]: + """ + Allows a coldkey to set children-keys. + + Parameters: + wallet: bittensor wallet instance. + hotkey: The `SS58` address of the neuron's hotkey. + netuid: The netuid value. + children: A list of children with their proportions. + 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. + + Returns: + Tuple[bool, str]: + - True and a success message if the extrinsic is successfully submitted or processed. + - False and an error message if the submission fails or the wallet cannot be unlocked. + + Raises: + DuplicateChild: There are duplicates in the list of children. + InvalidChild: Child is the hotkey. + NonAssociatedColdKey: The coldkey does not own the hotkey or the child is the same as the hotkey. + NotEnoughStakeToSetChildkeys: Parent key doesn't have minimum own stake. + ProportionOverflow: The sum of the proportions does exceed uint64. + RegistrationNotPermittedOnRootSubnet: Attempting to register a child on the root network. + SubNetworkDoesNotExist: Attempting to register to a non-existent network. + TooManyChildren: Too many children in request. + TxRateLimitExceeded: Hotkey hit the rate limit. + bittensor_wallet.errors.KeyFileError: Failed to decode keyfile data. + bittensor_wallet.errors.PasswordError: Decryption failed or wrong password for decryption provided. + """ + return await set_children_extrinsic( + subtensor=self, + wallet=wallet, + hotkey=hotkey, + netuid=netuid, + children=children, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + async def set_delegate_take( + self, + wallet: "Wallet", + hotkey_ss58: str, + take: float, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + ) -> tuple[bool, str]: + """ + Sets the delegate 'take' percentage for a neuron identified by its hotkey. + The 'take' represents the percentage of rewards that the delegate claims from its nominators' stakes. + + Parameters: + wallet: bittensor wallet instance. + hotkey_ss58: The ``SS58`` address of the neuron's hotkey. + take: Percentage reward for the delegate. + 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. + + Returns: + Tuple[bool, str]: + - True and a success message if the extrinsic is successfully submitted or processed. + - False and an error message if the submission fails or the wallet cannot be unlocked. + + Raises: + DelegateTakeTooHigh: Delegate take is too high. + DelegateTakeTooLow: Delegate take is too low. + DelegateTxRateLimitExceeded: A transactor exceeded the rate limit for delegate transaction. + HotKeyAccountNotExists: The hotkey does not exist. + NonAssociatedColdKey: Request to stake, unstake, or subscribe is made by a coldkey that is not associated + with the hotkey account. + bittensor_wallet.errors.PasswordError: Decryption failed or wrong password for decryption provided. + bittensor_wallet.errors.KeyFileError: Failed to decode keyfile data. + + The delegate take is a critical parameter in the network's incentive structure, influencing the distribution of + rewards among neurons and their nominators. + """ + # u16 representation of the take + take_u16 = int(take * 0xFFFF) + + current_take = await self.get_delegate_take(hotkey_ss58) + current_take_u16 = int(current_take * 0xFFFF) + + if current_take_u16 == take_u16: + logging.info(":white_heavy_check_mark: [green]Already Set[/green]") + return True, "" + + logging.info(f"Updating {hotkey_ss58} take: current={current_take} new={take}") + + extrinsic_call = ( + increase_take_extrinsic + if current_take_u16 < take_u16 + else decrease_take_extrinsic + ) + + success, message = await extrinsic_call( + subtensor=self, + wallet=wallet, + hotkey_ss58=hotkey_ss58, + take=take_u16, + period=period, + raise_error=raise_error, + wait_for_finalization=wait_for_finalization, + wait_for_inclusion=wait_for_inclusion, + ) + + if success: + logging.info(":white_heavy_check_mark: [green]Take Updated[/green]") + + return success, message + + async def set_subnet_identity( + self, + wallet: "Wallet", + netuid: int, + subnet_identity: SubnetIdentity, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + ) -> tuple[bool, str]: + """ + Sets the identity of a subnet for a specific wallet and network. + + Parameters: + wallet: The wallet instance that will authorize the transaction. + netuid: The unique ID of the network on which the operation takes place. + subnet_identity: The identity data of the subnet including attributes like name, GitHub repository, contact, + URL, discord, description, and any additional metadata. + 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. + + Returns: + Tuple[bool, str]: + - True and a success message if the extrinsic is successfully submitted or processed. + - False and an error message if the submission fails or the wallet cannot be unlocked. + """ + return await set_subnet_identity_extrinsic( + subtensor=self, + wallet=wallet, + netuid=netuid, + 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.logo_url, + discord=subnet_identity.discord, + description=subnet_identity.description, + additional=subnet_identity.additional, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + async def set_weights( + self, + wallet: "Wallet", + netuid: int, + uids: Union[NDArray[np.int64], "torch.LongTensor", list], + weights: Union[NDArray[np.float32], "torch.FloatTensor", list], + block_time: float = 12.0, + commit_reveal_version: int = 4, + max_retries: int = 5, + version_key: int = version_as_int, + period: Optional[int] = 8, + raise_error: bool = True, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + ): + """ + Sets the weight vector for a neuron acting as a validator, specifying the weights assigned to subnet miners + based on their performance evaluation. + + This method allows subnet validators to submit their weight vectors, which rank the value of each subnet miner's + work. These weight vectors are used by the Yuma Consensus algorithm to compute emissions for both validators and + miners. + + Parameters: + wallet: The wallet associated with the subnet validator setting the weights. + netuid: The unique identifier of the subnet. + uids: The list of subnet miner neuron UIDs that the weights are being set for. + weights: The corresponding weights to be set for each UID, representing the validator's evaluation of each + miner's performance. + block_time: The number of seconds for block duration. + commit_reveal_version: The version of the chain commit-reveal protocol to use. + max_retries: The number of maximum attempts to set weights. + version_key: Version key for compatibility with the network. + 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. + + Returns: + Tuple[bool, str]: + - True and a success message if the extrinsic is successfully submitted or processed. + - False and an error message if the submission fails or the wallet cannot be unlocked. + + This function is crucial in the Yuma Consensus mechanism, where each validator's weight vector contributes to + the overall weight matrix used to calculate emissions and maintain network consensus. + + Notes: + See + """ + + async def _blocks_weight_limit() -> bool: + bslu, wrl = await asyncio.gather( + self.blocks_since_last_update(netuid, uid), + self.weights_rate_limit(netuid), + ) + return bslu > wrl + + retries = 0 + success = False + message = "No attempt made. Perhaps it is too soon to set weights!" + if ( + uid := await self.get_uid_for_hotkey_on_subnet( + wallet.hotkey.ss58_address, netuid + ) + ) is None: + return ( + False, + f"Hotkey {wallet.hotkey.ss58_address} not registered in subnet {netuid}", + ) + + if await self.commit_reveal_enabled(netuid=netuid): + # go with `commit reveal v3` extrinsic + + while ( + retries < max_retries + and success is False + and await _blocks_weight_limit() + ): + logging.info( + f"Committing weights for subnet [blue]{netuid}[/blue]. " + f"Attempt [blue]{retries + 1}[blue] of [green]{max_retries}[/green]." + ) + try: + success, message = await commit_reveal_extrinsic( + subtensor=self, + wallet=wallet, + netuid=netuid, + uids=uids, + weights=weights, + block_time=block_time, + commit_reveal_version=commit_reveal_version, + version_key=version_key, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + except Exception as e: + logging.error(f"Error setting weights: {e}") + retries += 1 + + return success, message + else: + # go with classic `set weights extrinsic` + + while ( + retries < max_retries + and success is False + and await _blocks_weight_limit() + ): + try: + logging.info( + f"Setting weights for subnet #[blue]{netuid}[/blue]. " + f"Attempt [blue]{retries + 1}[/blue] of [green]{max_retries}[/green]." + ) + success, message = await set_weights_extrinsic( + subtensor=self, + wallet=wallet, + netuid=netuid, + uids=uids, + weights=weights, + version_key=version_key, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + except Exception as e: + logging.error(f"Error setting weights: {e}") + retries += 1 + + return success, message + + async def serve_axon( + self, + netuid: int, + axon: "Axon", + certificate: Optional[Certificate] = None, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + ) -> bool: + """ + Registers an ``Axon`` serving endpoint on the Bittensor network for a specific neuron. + + This function is used to set up the Axon, a key component of a neuron that handles incoming queries and data + processing tasks. + + Parameters: + netuid: The unique identifier of the subnetwork. + axon: The Axon instance to be registered for serving. + certificate: Certificate to use for TLS. If ``None``, no TLS will be used. + 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. + + Returns: + bool: ``True`` if the Axon serve registration is successful, False otherwise. + + By registering an Axon, the neuron becomes an active part of the network's distributed computing infrastructure, + contributing to the collective intelligence of Bittensor. + """ + return await serve_axon_extrinsic( + subtensor=self, + netuid=netuid, + axon=axon, + certificate=certificate, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + async def set_commitment( + self, + wallet: "Wallet", + netuid: int, + data: str, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + ) -> bool: + """ + Commits arbitrary data to the Bittensor network by publishing metadata. + + This method allows neurons to publish arbitrary data to the blockchain, which can be used for various purposes + such as sharing model updates, configuration data, or other network-relevant information. + + Parameters: + wallet (bittensor_wallet.Wallet): The wallet associated with the neuron committing the data. + netuid (int): The unique identifier of the subnetwork. + data (str): The data to be committed to the network. + 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. + + Returns: + bool: `True` if the commitment was successful, `False` otherwise. + + Example: + # Commit some data to subnet 1 + success = await subtensor.commit(wallet=my_wallet, netuid=1, data="Hello Bittensor!") + + # Commit with custom period + success = await subtensor.commit(wallet=my_wallet, netuid=1, data="Model update v2.0", period=100) + + Note: See + """ + return await publish_metadata( + subtensor=self, + wallet=wallet, + netuid=netuid, + data_type=f"Raw{len(data)}", + data=data.encode(), + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + async def start_call( + self, + wallet: "Wallet", + netuid: int, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, + ) -> tuple[bool, str]: + """ + Submits a start_call extrinsic to the blockchain, to trigger the start call process for a subnet (used to start + a new subnet's emission mechanism). + + Parameters: + wallet: The wallet used to sign the extrinsic (must be unlocked). + netuid: The UID of the target subnet for which the call is being initiated. + 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. + + Returns: + Tuple[bool, str]: + - True and a success message if the extrinsic is successfully submitted or processed. + - False and an error message if the submission fails or the wallet cannot be unlocked. + """ + return await start_call_extrinsic( + subtensor=self, + wallet=wallet, + netuid=netuid, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + async def swap_stake( + self, + wallet: "Wallet", + hotkey_ss58: str, + origin_netuid: int, + destination_netuid: int, + amount: Balance, + safe_swapping: bool = False, + allow_partial_stake: bool = False, + rate_tolerance: float = 0.005, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + ) -> bool: + """ + Moves stake between subnets while keeping the same coldkey-hotkey pair ownership. + Like subnet hopping - same owner, same hotkey, just changing which subnet the stake is in. + + Parameters: + wallet: The wallet to swap stake from. + hotkey_ss58: The SS58 address of the hotkey whose stake is being swapped. + origin_netuid: The netuid from which stake is removed. + destination_netuid: The netuid to which stake is added. + amount: The amount to swap. + safe_swapping: If true, enables price safety checks to protect against fluctuating prices. The swap + will only execute if the price ratio between subnets doesn't exceed the rate tolerance. + allow_partial_stake: If true and safe_staking is enabled, allows partial stake swaps when the full amount + would exceed the price tolerance. If false, the entire swap fails if it would exceed the tolerance. + rate_tolerance: The maximum allowed increase in the price ratio between subnets + (origin_price/destination_price). For example, 0.005 = 0.5% maximum increase. Only used when + safe_staking is True. + 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. + + Returns: + success: True if the extrinsic was successful. + + The price ratio for swap_stake in safe mode is calculated as: origin_subnet_price / destination_subnet_price + When safe_staking is enabled, the swap will only execute if: + - With allow_partial_stake=False: The entire swap amount can be executed without the price ratio increasing + more than rate_tolerance. + - With allow_partial_stake=True: A partial amount will be swapped up to the point where the price ratio + would increase by rate_tolerance. + """ + amount = check_and_convert_to_balance(amount) + return await swap_stake_extrinsic( + subtensor=self, + wallet=wallet, + hotkey_ss58=hotkey_ss58, + origin_netuid=origin_netuid, + destination_netuid=destination_netuid, + amount=amount, + safe_swapping=safe_swapping, + allow_partial_stake=allow_partial_stake, + rate_tolerance=rate_tolerance, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + async def toggle_user_liquidity( + self, + wallet: "Wallet", + netuid: int, + enable: bool, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + ) -> tuple[bool, str]: + """Allow to toggle user liquidity for specified subnet. + + Parameters: + wallet: The wallet used to sign the extrinsic (must be unlocked). + netuid: The UID of the target subnet for which the call is being initiated. + enable: Boolean indicating whether to enable user liquidity. + 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 extrinsic to be included in a block. + wait_for_finalization: Whether to wait for finalization of the extrinsic. + + Returns: + Tuple[bool, str]: + - True and a success message if the extrinsic is successfully submitted or processed. + - False and an error message if the submission fails or the wallet cannot be unlocked. + + Note: The call can be executed successfully by the subnet owner only. + """ + return await toggle_user_liquidity_extrinsic( + subtensor=self, + wallet=wallet, + netuid=netuid, + enable=enable, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + async def transfer( + self, + wallet: "Wallet", + destination: str, + amount: Optional[Balance], + transfer_all: bool = False, + keep_alive: bool = True, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, + ) -> bool: + """ + Transfer token of amount to destination. + + Parameters: + wallet: Source wallet for the transfer. + destination: Destination address for the transfer. + amount: Number of tokens to transfer. `None` is transferring all. + transfer_all: Flag to transfer all tokens. Default is `False`. + keep_alive: Flag to keep the connection alive. Default is `True`. + 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 extrinsic to be included in a block. + wait_for_finalization: Whether to wait for finalization of the extrinsic. + + Returns: + `True` if the transferring was successful, otherwise `False`. + """ + if amount is not None: + amount = check_and_convert_to_balance(amount) + return await transfer_extrinsic( + subtensor=self, + wallet=wallet, + destination=destination, + amount=amount, + transfer_all=transfer_all, + keep_alive=keep_alive, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + async def transfer_stake( + self, + wallet: "Wallet", + destination_coldkey_ss58: str, + hotkey_ss58: str, + origin_netuid: int, + destination_netuid: int, + amount: Balance, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + ) -> bool: + """ + Transfers stake from one subnet to another while changing the coldkey owner. + + Parameters: + wallet: The wallet to transfer stake from. + destination_coldkey_ss58: The destination coldkey SS58 address. + hotkey_ss58: The hotkey SS58 address associated with the stake. + origin_netuid: The source subnet UID. + destination_netuid: The destination subnet UID. + amount: Amount to transfer. + 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 extrinsic to be included in a block. + wait_for_finalization: Whether to wait for finalization of the extrinsic. + + Returns: + success: True if the transfer was successful. + """ + amount = check_and_convert_to_balance(amount) + return await transfer_stake_extrinsic( + subtensor=self, + wallet=wallet, + destination_coldkey_ss58=destination_coldkey_ss58, + hotkey_ss58=hotkey_ss58, + origin_netuid=origin_netuid, + destination_netuid=destination_netuid, + amount=amount, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + async def unstake( + self, + wallet: "Wallet", + netuid: int, + hotkey_ss58: str, + amount: Balance, + allow_partial_stake: bool = False, + safe_unstaking: bool = False, + rate_tolerance: float = 0.005, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + ) -> bool: + """ + Removes a specified amount of stake from a single hotkey account. This function is critical for adjusting + individual neuron stakes within the Bittensor network. + + Parameters: + wallet: The wallet associated with the neuron from which the stake is being removed. + netuid: The unique identifier of the subnet. + hotkey_ss58: The ``SS58`` address of the hotkey account to unstake from. + amount: The amount of alpha to unstake. If not specified, unstakes all. Alpha amount. + allow_partial_stake: If true and safe_staking is enabled, allows partial unstaking when + the full amount would exceed the price tolerance. If false, the entire unstake fails if it would + exceed the tolerance. + rate_tolerance: The maximum allowed price change ratio when unstaking. For example, + 0.005 = 0.5% maximum price decrease. Only used when safe_staking is True. + safe_unstaking: If true, enables price safety checks to protect against fluctuating prices. The unstake + will only execute if the price change doesn't exceed the rate tolerance. + 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 extrinsic to be included in a block. + wait_for_finalization: Whether to wait for finalization of the extrinsic. + + Returns: + bool: `True` if the unstaking process is successful, False otherwise. + + This function supports flexible stake management, allowing neurons to adjust their network participation and + potential reward accruals. + """ + amount = check_and_convert_to_balance(amount) + return await unstake_extrinsic( + subtensor=self, + wallet=wallet, + hotkey_ss58=hotkey_ss58, + netuid=netuid, + amount=amount, + allow_partial_stake=allow_partial_stake, + rate_tolerance=rate_tolerance, + safe_unstaking=safe_unstaking, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + async def unstake_all( + self, + wallet: "Wallet", + hotkey: str, + netuid: int, + rate_tolerance: Optional[float] = 0.005, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + ) -> tuple[bool, str]: + """Unstakes all TAO/Alpha associated with a hotkey from the specified subnets on the Bittensor network. + + Parameters: + wallet: The wallet of the stake owner. + hotkey: The SS58 address of the hotkey to unstake from. + netuid: The unique identifier of the subnet. + rate_tolerance: The maximum allowed price change ratio when unstaking. For example, 0.005 = 0.5% maximum + price decrease. If not passed (None), then unstaking goes without price limit. Default is 0.005. + 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 extrinsic to be included in a block. + wait_for_finalization: Whether to wait for finalization of the extrinsic. + + Returns: + tuple[bool, str]: + A tuple containing: + - `True` and a success message if the unstake operation succeeded; + - `False` and an error message otherwise. + + Example: + # If you would like to unstake all stakes in all subnets safely, use default `rate_tolerance` or pass your + value: + import bittensor as bt + + subtensor = bt.AsyncSubtensor() + wallet = bt.Wallet("my_wallet") + netuid = 14 + hotkey = "5%SOME_HOTKEY_WHERE_IS_YOUR_STAKE_NOW%" + + wallet_stakes = await subtensor.get_stake_info_for_coldkey(coldkey_ss58=wallet.coldkey.ss58_address) + + for stake in wallet_stakes: + result = await subtensor.unstake_all( + wallet=wallet, + hotkey_ss58=stake.hotkey_ss58, + netuid=stake.netuid, + ) + print(result) + + # If you would like to unstake all stakes in all subnets unsafely, use `rate_tolerance=None`: + import bittensor as bt + + subtensor = bt.AsyncSubtensor() + wallet = bt.Wallet("my_wallet") + netuid = 14 + hotkey = "5%SOME_HOTKEY_WHERE_IS_YOUR_STAKE_NOW%" + + wallet_stakes = await subtensor.get_stake_info_for_coldkey(coldkey_ss58=wallet.coldkey.ss58_address) + + for stake in wallet_stakes: + result = await subtensor.unstake_all( + wallet=wallet, + hotkey_ss58=stake.hotkey_ss58, + netuid=stake.netuid, + rate_tolerance=None, + ) + print(result) + """ + if netuid != 0: + logging.debug( + f"Unstaking without Alpha price control from subnet [blue]#{netuid}[/blue]." + ) + return await unstake_all_extrinsic( + subtensor=self, + wallet=wallet, + hotkey=hotkey, + netuid=netuid, + rate_tolerance=rate_tolerance, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + async def unstake_multiple( + self, + wallet: "Wallet", + netuids: list[int], + hotkey_ss58s: list[str], + amounts: Optional[list[Balance]] = None, + unstake_all: bool = False, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + ) -> bool: + """ + Performs batch unstaking from multiple hotkey accounts, allowing a neuron to reduce its staked amounts + efficiently. This function is useful for managing the distribution of stakes across multiple neurons. + + Parameters: + wallet: The wallet linked to the coldkey from which the stakes are being withdrawn. + netuids: Subnets unique IDs. + hotkey_ss58s: A list of hotkey `SS58` addresses to unstake from. + amounts: The amounts of TAO to unstake from each hotkey. If not provided, unstakes all. + unstake_all: If true, unstakes all tokens. Default is `False`. If `True` amounts are ignored. + 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 extrinsic to be included in a block. + wait_for_finalization: Whether to wait for finalization of the extrinsic. + + Returns: + bool: `True` if the batch unstaking is successful, False otherwise. + + This function allows for strategic reallocation or withdrawal of stakes, aligning with the dynamic stake + management aspect of the Bittensor network. + """ + return await unstake_multiple_extrinsic( + subtensor=self, + wallet=wallet, + netuids=netuids, + hotkey_ss58s=hotkey_ss58s, + amounts=amounts, + unstake_all=unstake_all, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + +async def get_async_subtensor( + network: Optional[str] = None, + config: Optional["Config"] = None, + _mock: bool = False, + log_verbose: bool = False, +) -> "AsyncSubtensor": + """Factory method to create an initialized AsyncSubtensor. + Mainly useful for when you don't want to run `await subtensor.initialize()` after instantiation. + """ + sub = AsyncSubtensor( + network=network, config=config, _mock=_mock, log_verbose=log_verbose + ) + await sub.initialize() + return sub diff --git a/bittensor/core/axon.py b/bittensor/core/axon.py new file mode 100644 index 0000000000..76ecc14a4b --- /dev/null +++ b/bittensor/core/axon.py @@ -0,0 +1,1564 @@ +"""Create and initialize Axon, which services the forward and backward requests from other neurons.""" + +import argparse +import asyncio +import contextlib +import copy +import inspect +import threading +import time +import traceback +import typing +import uuid +import warnings +from inspect import signature, Signature, Parameter +from typing import Any, Awaitable, Callable, Optional, Tuple + +from async_substrate_interface.utils import json +import uvicorn +from bittensor_wallet import Wallet, Keypair +from fastapi import APIRouter, Depends, FastAPI +from fastapi.responses import JSONResponse +from fastapi.routing import serialize_response +from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint +from starlette.requests import Request +from starlette.responses import Response + +from bittensor.core.chain_data import AxonInfo +from bittensor.core.config import Config +from bittensor.core.errors import ( + BlacklistedException, + InvalidRequestNameError, + NotVerifiedException, + PostProcessException, + PriorityException, + SynapseDendriteNoneException, + SynapseException, + SynapseParsingError, + UnknownSynapseError, +) +from bittensor.core.settings import DEFAULTS, version_as_int +from bittensor.core.stream import StreamingSynapse +from bittensor.core.synapse import Synapse, TerminalInfo +from bittensor.core.threadpool import PriorityThreadPoolExecutor +from bittensor.utils import networking, Certificate +from bittensor.utils.axon_utils import allowed_nonce_window_ns, calculate_diff_seconds +from bittensor.utils.btlogging import logging + +# Just for annotation checker +if typing.TYPE_CHECKING: + from bittensor.core.subtensor import Subtensor + +# Latest version with old style nonce structure (this in not a current SDK version) +V_7_2_0 = 7002000 + + +class FastAPIThreadedServer(uvicorn.Server): + """ + The ``FastAPIThreadedServer`` class is a specialized server implementation for the Axon server in the Bittensor + network. + It extends the functionality of :func:`uvicorn.Server` to run the FastAPI application in a separate thread, allowing + the Axon server to handle HTTP requests concurrently and non-blocking. + + This class is designed to facilitate the integration of FastAPI with the Axon's asynchronous architecture, ensuring + efficient and scalable handling of network requests. + + Importance and Functionality + Threaded Execution + The class allows the FastAPI application to run in a separate thread, enabling concurrent handling of HTTP + requests which is crucial for the performance and scalability of the Axon server. + + Seamless Integration + By running FastAPI in a threaded manner, this class ensures seamless integration of FastAPI's capabilities + with the Axon server's asynchronous and multi-threaded architecture. + + Controlled Server Management + The methods start and stop provide controlled management of the server's lifecycle, ensuring that the server + can be started and stopped as needed, which is vital for maintaining the Axon server's reliability and + availability. + + Signal Handling + Overriding the default signal handlers prevents potential conflicts with the Axon server's main application + flow, ensuring stable operation in various network conditions. + + Use Cases + Starting the Server + When the Axon server is initialized, it can use this class to start the FastAPI application in a separate + thread, enabling it to begin handling HTTP requests immediately. + + Stopping the Server + During shutdown or maintenance of the Axon server, this class can be used to stop the FastAPI application + gracefully, ensuring that all resources are properly released. + + Example Usage:: + + self.app = FastAPI() + log_level = "trace" + self.fast_config = uvicorn.Config(self.app, host="0.0.0.0", port=self.config.axon.port, log_level=log_level) + self.fast_server = FastAPIThreadedServer(config=self.fast_config) + self.fast_server.start() + # do something + self.fast_server.stop() + + Args: + should_exit (bool): Flag to indicate whether the server should stop running. + is_running (bool): Flag to indicate whether the server is currently running. + + The server overrides the default signal handlers to prevent interference with the main application flow and provides + methods to start and stop the server in a controlled manner. + """ + + should_exit: bool = False + is_running: bool = False + + def install_signal_handlers(self): + """ + Overrides the default signal handlers provided by ``uvicorn.Server``. This method is essential to ensure that + the signal handling in the threaded server does not interfere with the main application's flow, especially in a + complex asynchronous environment like the Axon server. + """ + + @contextlib.contextmanager + def run_in_thread(self): + """ + Manages the execution of the server in a separate thread, allowing the FastAPI application to run asynchronously + without blocking the main thread of the Axon server. This method is a key component in enabling concurrent + request handling in the Axon server. + + Yields: + None: This method yields control back to the caller while the server is running in the background thread. + """ + thread = threading.Thread(target=self.run, daemon=True) + thread.start() + try: + while not self.started: + time.sleep(1e-3) + yield + finally: + self.should_exit = True + thread.join() + + def _wrapper_run(self): + """ + A wrapper method for the :func:`run_in_thread` context manager. This method is used internally by the ``start`` + method to initiate the server's execution in a separate thread. + """ + with self.run_in_thread(): + while not self.should_exit: + time.sleep(1e-3) + + def start(self): + """ + Starts the FastAPI server in a separate thread if it is not already running. This method sets up the server to + handle HTTP requests concurrently, enabling the Axon server to efficiently manage incoming network requests. + + The method ensures that the server starts running in a non-blocking manner, allowing the Axon server to continue + its other operations seamlessly. + """ + if not self.is_running: + self.should_exit = False + thread = threading.Thread(target=self._wrapper_run, daemon=True) + thread.start() + self.is_running = True + + def stop(self): + """ + Signals the FastAPI server to stop running. This method sets the :func:`should_exit` flag to ``True``, + indicating that the server should cease its operations and exit the running thread. + + Stopping the server is essential for controlled shutdowns and resource management in the Axon server, especially + during maintenance or when redeploying with updated configurations. + """ + if self.is_running: + self.should_exit = True + + +class Axon: + """ + The ``Axon`` class in Bittensor is a fundamental component that serves as the server-side interface for a neuron + within the Bittensor network. + + This class is responsible for managing + incoming requests from other neurons and implements various mechanisms to ensure efficient + and secure network interactions. + + An axon relies on a FastAPI router to create endpoints for different message types. These + endpoints are crucial for handling various request types that a neuron might receive. The + class is designed to be flexible and customizable, allowing users to specify custom rules + for forwarding, blacklisting, prioritizing, and verifying incoming requests. The class also + includes internal mechanisms to manage a thread pool, supporting concurrent handling of + requests with defined priority levels. + + Methods in this class are equipped to deal with incoming requests from various scenarios in the + network and serve as the server face for a neuron. It accepts multiple arguments, like wallet, + configuration parameters, ip address, server binding port, external ip, external port and max + workers. Key methods involve managing and operating the FastAPI application router, including + the attachment and operation of endpoints. + + Key Features: + + - FastAPI router integration for endpoint creation and management. + - Customizable request handling including forwarding, blacklisting, and prioritization. + - Verification of incoming requests against custom-defined functions. + - Thread pool management for concurrent request handling. + - Command-line argument support for user-friendly program interaction. + + Example Usage:: + + import bittensor + # Define your custom synapse class + class MySynapse( bittensor.Synapse ): + input: int = 1 + output: int = None + + # Define a custom request forwarding function using your synapse class + def forward( synapse: MySynapse ) -> MySynapse: + # Apply custom logic to synapse and return it + synapse.output = 2 + return synapse + + # Define a custom request verification function + def verify_my_synapse( synapse: MySynapse ): + # Apply custom verification logic to synapse + # Optionally raise Exception + assert synapse.input == 1 + ... + + # Define a custom request blacklist function + def blacklist_my_synapse( synapse: MySynapse ) -> bool: + # Apply custom blacklist + return False ( if non blacklisted ) or True ( if blacklisted ) + + # Define a custom request priority function + def prioritize_my_synapse( synapse: MySynapse ) -> float: + # Apply custom priority + return 1.0 + + # Initialize Axon object with a custom configuration + my_axon = bittensor.Axon( + config=my_config, + wallet=my_wallet, + port=9090, + ip="192.0.2.0", + external_ip="203.0.113.0", + external_port=7070 + ) + + # Attach the endpoint with the specified verification and forward functions. + my_axon.attach( + forward_fn = forward_my_synapse, + verify_fn = verify_my_synapse, + blacklist_fn = blacklist_my_synapse, + priority_fn = prioritize_my_synapse + ) + + # Serve and start your axon. + my_axon.serve( + netuid = ... + subtensor = ... + ).start() + + # If you have multiple forwarding functions, you can chain attach them. + my_axon.attach( + forward_fn = forward_my_synapse, + verify_fn = verify_my_synapse, + blacklist_fn = blacklist_my_synapse, + priority_fn = prioritize_my_synapse + ).attach( + forward_fn = forward_my_synapse_2, + verify_fn = verify_my_synapse_2, + blacklist_fn = blacklist_my_synapse_2, + priority_fn = prioritize_my_synapse_2 + ).serve( + netuid = ... + subtensor = ... + ).start() + + Args: + wallet (Optional[bittensor_wallet.Wallet]): Wallet with hotkey and coldkeypub. + config (Optional[bittensor.core.config.Config]): Configuration parameters for the axon. + port (Optional[int]): Port for server binding. + ip (Optional[str]): Binding IP address. + external_ip (Optional[str]): External IP address to broadcast. + external_port (Optional[int]): External port to broadcast. + max_workers (Optional[int]): Number of active threads for request handling. + + Returns: + bittensor.core.axon.Axon: An instance of the axon class configured as per the provided arguments. + + Note: + This class is a core part of Bittensor's decentralized network for machine intelligence, + allowing neurons to communicate effectively and securely. + + Importance and Functionality + Endpoint Registration + This method dynamically registers API endpoints based on the Synapse used, allowing the Axon to respond to + specific types of requests and synapses. + + Customization of Request Handling + By attaching different functions, the Axon can customize how it + handles, verifies, prioritizes, and potentially blocks incoming requests, making it adaptable to various + network scenarios. + + Security and Efficiency + The method contributes to both the security (via verification and blacklisting) and efficiency (via + prioritization) of request handling, which are crucial in a decentralized network environment. + + Flexibility + The ability to define custom functions for different aspects of request handling provides great flexibility, + allowing the Axon to be tailored to specific needs and use cases within the Bittensor network. + + Error Handling and Validation + The method ensures that the attached functions meet the required + signatures, providing error handling to prevent runtime issues. + """ + + def __init__( + self, + wallet: Optional["Wallet"] = None, + config: Optional["Config"] = None, + port: Optional[int] = None, + ip: Optional[str] = None, + external_ip: Optional[str] = None, + external_port: Optional[int] = None, + max_workers: Optional[int] = None, + ): + """Creates a new bittensor.Axon object from passed arguments. + + Args: + config (:obj:`Optional[bittensor.core.config.Config]`): bittensor.Axon.config() + wallet (:obj:`Optional[bittensor_wallet.Wallet]`): bittensor wallet with hotkey and coldkeypub. + port (:type:`Optional[int]`): Binding port. + ip (:type:`Optional[str]`): Binding ip. + external_ip (:type:`Optional[str]`): The external ip of the server to broadcast to the network. + external_port (:type:`Optional[int]`): The external port of the server to broadcast to the network. + max_workers (:type:`Optional[int]`): Used to create the threadpool if not passed, specifies the number of + active threads servicing requests. + """ + # Build and check config. + if config is None: + config = Axon.config() + config = copy.deepcopy(config) + config.axon.ip = ip or config.axon.ip + config.axon.port = port or config.axon.port + config.axon.external_ip = external_ip or config.axon.external_ip + config.axon.external_port = external_port or config.axon.external_port + config.axon.max_workers = max_workers or config.axon.max_workers + Axon.check_config(config) + self._config = config + + # Get wallet or use default. + self.wallet: Wallet = wallet or Wallet(config=self._config) + + # Build axon objects. + self.uuid = str(uuid.uuid1()) + self.ip: str = self._config.axon.ip + self.port: int = self._config.axon.port + self.external_ip: str = ( + self._config.axon.external_ip + if self._config.axon.external_ip is not None + else networking.get_external_ip() + ) + self.external_port: int = ( + self._config.axon.external_port + if self._config.axon.external_port is not None + else self._config.axon.port + ) + self.full_address = ( + str(self._config.axon.ip) + ":" + str(self._config.axon.port) + ) + self.started = False + + # Build middleware + self.thread_pool = PriorityThreadPoolExecutor( + max_workers=self._config.axon.max_workers + ) + self.nonces: dict[str, int] = {} + + # Request default functions. + self.forward_class_types: dict[str, list[Signature]] = {} + self.blacklist_fns: dict[str, Optional[Callable]] = {} + self.priority_fns: dict[str, Optional[Callable]] = {} + self.forward_fns: dict[str, Optional[Callable]] = {} + self.verify_fns: dict[str, Optional[Callable]] = {} + + # Instantiate FastAPI + self.app = FastAPI() + log_level = "trace" if logging.__trace_on__ else "critical" + self.fast_config = uvicorn.Config( + self.app, + host="0.0.0.0", + log_level=log_level, + loop="none", + port=self._config.axon.port, + ) + self.fast_server = FastAPIThreadedServer(config=self.fast_config) + self.router = APIRouter() + self.app.include_router(self.router) + + # Build ourselves as the middleware. + self.middleware_cls = AxonMiddleware + self.app.add_middleware(self.middleware_cls, axon=self) + + # Attach default forward. + def ping(r: Synapse) -> Synapse: + return r + + self.attach( + forward_fn=ping, verify_fn=None, blacklist_fn=None, priority_fn=None + ) + + def info(self) -> "AxonInfo": + """Returns the axon info object associated with this axon.""" + return AxonInfo( + version=version_as_int, + ip=self.external_ip, + ip_type=networking.ip_version(self.external_ip), + port=self.external_port, + hotkey=self.wallet.hotkey.ss58_address, + coldkey=self.wallet.coldkeypub.ss58_address, + protocol=4, + placeholder1=0, + placeholder2=0, + ) + + def attach( + self, + forward_fn: Callable, + blacklist_fn: Optional[Callable] = None, + priority_fn: Optional[Callable] = None, + verify_fn: Optional[Callable] = None, + ) -> "Axon": + """ + + Attaches custom functions to the Axon server for handling incoming requests. This method enables + the ``Axon`` to define specific behaviors for request forwarding, verification, blacklisting, and + prioritization, thereby customizing its interaction within the Bittensor network. + + Registers an API endpoint to the FastAPI application router. + It uses the name of the first argument of the :func:`forward_fn` function as the endpoint name. + + The :func:`attach` method in the Bittensor framework's axon class is a crucial function for registering + API endpoints to the Axon's FastAPI application router. This method allows the Axon server to + define how it handles incoming requests by attaching functions for forwarding, verifying, + blacklisting, and prioritizing requests. It's a key part of customizing the server's behavior + and ensuring efficient and secure handling of requests within the Bittensor network. + + Args: + forward_fn (Callable): Function to be called when the API endpoint is accessed. It should have at least one + argument. + blacklist_fn (Optional[Callable]): Function to filter out undesired requests. It should take the same + arguments as :func:`forward_fn` and return a boolean value. Defaults to ``None``, meaning no blacklist + filter will be used. + priority_fn (Optional[Callable]): Function to rank requests based on their priority. It should take the same + arguments as :func:`forward_fn` and return a numerical value representing the request's priority. + Defaults to ``None``, meaning no priority sorting will be applied. + verify_fn (Optional[Callable]): Function to verify requests. It should take the same arguments as + :func:`forward_fn` and return a boolean value. If ``None``, :func:`self.default_verify` function will be + used. + + Note: + The methods :func:`forward_fn`, :func:`blacklist_fn`, :func:`priority_fn`, and :func:`verify_fn` should be + designed to receive the same parameters. + + Raises: + AssertionError: If :func:`forward_fn` does not have the signature: ``forward( synapse: YourSynapse ) -> synapse``. + AssertionError: If :func:`blacklist_fn` does not have the signature: ``blacklist( synapse: YourSynapse ) -> bool``. + AssertionError: If :func:`priority_fn` does not have the signature: ``priority( synapse: YourSynapse ) -> float``. + AssertionError: If :func:`verify_fn` does not have the signature: ``verify( synapse: YourSynapse ) -> None``. + + Returns: + self: Returns the instance of the AxonServer class for potential method chaining. + + Example Usage:: + + def forward_custom(synapse: MyCustomSynapse) -> MyCustomSynapse: + # Custom logic for processing the request + return synapse + + def blacklist_custom(synapse: MyCustomSynapse) -> tuple[bool, str]: + return True, "Allowed!" + + def priority_custom(synapse: MyCustomSynapse) -> float: + return 1.0 + + def verify_custom(synapse: MyCustomSynapse): + # Custom logic for verifying the request + pass + + my_axon = bittensor.Axon(...) + my_axon.attach(forward_fn=forward_custom, verify_fn=verify_custom) + + Note: + The :func:`attach` method is fundamental in setting up the Axon server's request handling capabilities, + enabling it to participate effectively and securely in the Bittensor network. The flexibility + offered by this method allows developers to tailor the Axon's behavior to specific requirements and + use cases. + """ + forward_sig = signature(forward_fn) + try: + first_param = next(iter(forward_sig.parameters.values())) + except StopIteration: + raise ValueError( + "The forward_fn first argument must be a subclass of bittensor.Synapse, but it has no arguments" + ) + + param_class = first_param.annotation + assert issubclass(param_class, Synapse), ( + "The first argument of forward_fn must inherit from bittensor.Synapse" + ) + request_name = param_class.__name__ + + async def endpoint(*args, **kwargs): + start_time = time.time() + response = forward_fn(*args, **kwargs) + if isinstance(response, Awaitable): + response = await response + if isinstance(response, Synapse): + return await self.middleware_cls.synapse_to_response( + synapse=response, start_time=start_time + ) + else: + response_synapse = getattr(response, "synapse", None) + if response_synapse is None: + warnings.warn( + "The response synapse is None. The input synapse will be used as the response synapse. " + "Reliance on forward_fn modifying input synapse as a side-effects is deprecated. " + "Explicitly set `synapse` on response object instead.", + DeprecationWarning, + ) + # Replace with `return response` in next major version + response_synapse = args[0] + + return await self.middleware_cls.synapse_to_response( + synapse=response_synapse, + start_time=start_time, + response_override=response, + ) + + return_annotation = forward_sig.return_annotation + + if isinstance(return_annotation, type) and issubclass( + return_annotation, Synapse + ): + if issubclass( + return_annotation, + StreamingSynapse, + ): + warnings.warn( + "The forward_fn return annotation is a subclass of bittensor.StreamingSynapse. " + "Most likely the correct return annotation would be BTStreamingResponse." + ) + else: + return_annotation = JSONResponse + + endpoint.__signature__ = Signature( # type: ignore + parameters=list(forward_sig.parameters.values()), + return_annotation=return_annotation, + ) + + # Add the endpoint to the router, making it available on both GET and POST methods + self.router.add_api_route( + path=f"/{request_name}", + endpoint=endpoint, + methods=["GET", "POST"], + dependencies=[Depends(self.verify_body_integrity)], + ) + self.app.include_router(self.router) + + # Check the signature of blacklist_fn, priority_fn and verify_fn if they are provided + expected_params = [ + Parameter( + name="synapse", + kind=Parameter.POSITIONAL_OR_KEYWORD, + annotation=forward_sig.parameters[ + list(forward_sig.parameters)[0] + ].annotation, + ) + ] + if blacklist_fn: + blacklist_sig = Signature( + expected_params, return_annotation=Tuple[bool, str] + ) + assert signature(blacklist_fn) == blacklist_sig, ( + f"The blacklist_fn function must have the signature: blacklist( synapse: {request_name} ) -> tuple[bool, str]" + ) + if priority_fn: + priority_sig = Signature(expected_params, return_annotation=float) + assert signature(priority_fn) == priority_sig, ( + f"The priority_fn function must have the signature: priority( synapse: {request_name} ) -> float" + ) + if verify_fn: + verify_sig = Signature(expected_params, return_annotation=None) + assert signature(verify_fn) == verify_sig, ( + f"The verify_fn function must have the signature: verify( synapse: {request_name} ) -> None" + ) + + # Store functions in appropriate attribute dictionaries + self.forward_class_types[request_name] = param_class + self.blacklist_fns[request_name] = blacklist_fn + self.priority_fns[request_name] = priority_fn + self.verify_fns[request_name] = ( + verify_fn or self.default_verify + ) # Use 'default_verify' if 'verify_fn' is None + self.forward_fns[request_name] = forward_fn + + return self + + @classmethod + def config(cls) -> "Config": + """ + Parses the command-line arguments to form a Bittensor configuration object. + + Returns: + bittensor.core.config.Config: Configuration object with settings from command-line arguments. + """ + parser = argparse.ArgumentParser() + Axon.add_args(parser) # Add specific axon-related arguments + return Config(parser) + + @classmethod + def help(cls): + """Prints the help text (list of command-line arguments and their descriptions) to stdout.""" + parser = argparse.ArgumentParser() + Axon.add_args(parser) # Add specific axon-related arguments + print(cls.__new__.__doc__) # Print docstring of the class + parser.print_help() # Print parser's help text + + @classmethod + def add_args(cls, parser: argparse.ArgumentParser, prefix: Optional[str] = None): + """ + Adds AxonServer-specific command-line arguments to the argument parser. + + Args: + parser (argparse.ArgumentParser): Argument parser to which the arguments will be added. + prefix (Optional[str]): Prefix to add to the argument names. Defaults to None. + + Note: + Environment variables are used to define default values for the arguments. + """ + prefix_str = "" if prefix is None else prefix + "." + try: + # Add command-line arguments to the parser + parser.add_argument( + "--" + prefix_str + "axon.port", + type=int, + help="The local port this axon endpoint is bound to. i.e. 8091", + default=DEFAULTS.axon.port, + ) + parser.add_argument( + "--" + prefix_str + "axon.ip", + type=str, + help="""The local ip this axon binds to. ie. [::]""", + default=DEFAULTS.axon.ip, + ) + parser.add_argument( + "--" + prefix_str + "axon.external_port", + type=int, + required=False, + help="""The public port this axon broadcasts to the network. i.e. 8091""", + default=DEFAULTS.axon.external_port, + ) + parser.add_argument( + "--" + prefix_str + "axon.external_ip", + type=str, + required=False, + help="""The external ip this axon broadcasts to the network to. ie. [::]""", + default=DEFAULTS.axon.external_ip, + ) + parser.add_argument( + "--" + prefix_str + "axon.max_workers", + type=int, + help="""The maximum number connection handler threads working simultaneously on this endpoint. + The grpc server distributes new worker threads to service requests up to this number.""", + default=DEFAULTS.axon.max_workers, + ) + + except argparse.ArgumentError: + # Exception handling for re-parsing arguments + pass + + async def verify_body_integrity(self, request: "Request"): + """ + The ``verify_body_integrity`` method in the Bittensor framework is a key security function within the + Axon server's middleware. It is responsible for ensuring the integrity of the body of incoming HTTP + requests. + + It asynchronously verifies the integrity of the body of a request by comparing the hash of required fields + with the corresponding hashes provided in the request headers. This method is critical for ensuring + that the incoming request payload has not been altered or tampered with during transmission, establishing + a level of trust and security between the sender and receiver in the network. + + Args: + request (Request): The incoming FastAPI request object containing both headers and the request body. + + Returns: + dict: Returns the parsed body of the request as a dictionary if all the hash comparisons match, indicating + that the body is intact and has not been tampered with. + + Raises: + JSONResponse: Raises a JSONResponse with a 400 status code if any of the hash comparisons fail, indicating + a potential integrity issue with the incoming request payload. The response includes the detailed error + message specifying which field has a hash mismatch. + + This method performs several key functions: + + 1. Decoding and loading the request body for inspection. + 2. Gathering required field names for hash comparison from the Axon configuration. + 3. Loading and parsing the request body into a dictionary. + 4. Reconstructing the Synapse object and recomputing the hash for verification and logging. + 5. Comparing the recomputed hash with the hash provided in the request headers for verification. + + Note: + The integrity verification is an essential step in ensuring the security of the data exchange within the + Bittensor network. It helps prevent tampering and manipulation of data during transit, thereby maintaining + the reliability and trust in the network communication. + """ + # Await and load the request body, so we can inspect it + body = await request.body() + request_body = body.decode() if isinstance(body, bytes) else body + + request_name = request.url.path.split("/")[1] + + # Load the body dict and check if all required field hashes match + body_dict = json.loads(request_body) + + # Reconstruct the synapse object from the body dict and recompute the hash + syn = self.forward_class_types[request_name](**body_dict) # type: ignore + parsed_body_hash = syn.body_hash # Rehash the body from request + + body_hash = request.headers.get("computed_body_hash", "") + if parsed_body_hash != body_hash: + raise ValueError( + f"Hash mismatch between header body hash {body_hash} and parsed body hash {parsed_body_hash}" + ) + + # If body is good, return the parsed body so that it can be passed onto the route function + return body_dict + + @classmethod + def check_config(cls, config: "Config"): + """ + This method checks the configuration for the axon's port and wallet. + + Args: + config (bittensor.core.config.Config): The config object holding axon settings. + + Raises: + AssertionError: If the axon or external ports are not in range [1024, 65535] + """ + assert 1024 < config.axon.port < 65535, ( + "Axon port must be in range [1024, 65535]" + ) + + assert config.axon.external_port is None or ( + 1024 < config.axon.external_port < 65535 + ), "External port must be in range [1024, 65535]" + + def to_string(self): + """Provides a human-readable representation of the AxonInfo for this Axon.""" + return self.info().to_string() + + def __str__(self) -> str: + """Provides a human-readable representation of the Axon instance.""" + _started = "started" if self.started else "stopped" + _keys = list(self.forward_fns.keys()) + return f"Axon({self.ip}, {self.port}, {self.wallet.hotkey.ss58_address}, {_started}, {_keys})" + + def __repr__(self) -> str: + """ + Provides a machine-readable (unambiguous) representation of the Axon instance. + It is made identical to __str__ in this case. + """ + return self.__str__() + + def __del__(self): + """ + This magic method is called when the Axon object is about to be destroyed. + It ensures that the Axon server shuts down properly. + """ + self.stop() + + def start(self) -> "Axon": + """ + Starts the Axon server and its underlying FastAPI server thread, transitioning the state of the + Axon instance to ``started``. This method initiates the server's ability to accept and process + incoming network requests, making it an active participant in the Bittensor network. + + The start method triggers the FastAPI server associated with the Axon to begin listening for + incoming requests. It is a crucial step in making the neuron represented by this Axon operational + within the Bittensor network. + + Returns: + bittensor.core.axon.Axon: The Axon instance in the 'started' state. + + Example:: + + my_axon = bittensor.Axon(...) + ... # setup axon, attach functions, etc. + my_axon.start() # Starts the axon server + + Note: + After invoking this method, the Axon is ready to handle requests as per its configured endpoints and custom + logic. + """ + self.fast_server.start() + self.started = True + return self + + def stop(self) -> "Axon": + """ + Stops the Axon server and its underlying GRPC server thread, transitioning the state of the Axon + instance to ``stopped``. This method ceases the server's ability to accept new network requests, + effectively removing the neuron's server-side presence in the Bittensor network. + + By stopping the FastAPI server, the Axon ceases to listen for incoming requests, and any existing + connections are gracefully terminated. This function is typically used when the neuron is being + shut down or needs to temporarily go offline. + + Returns: + bittensor.core.axon.Axon: The Axon instance in the 'stopped' state. + + Example:: + + my_axon = bittensor.Axon(...) + my_axon.start() + ... + my_axon.stop() # Stops the axon server + + + Note: + It is advisable to ensure that all ongoing processes or requests are completed or properly handled before + invoking this method. + """ + self.fast_server.stop() + self.started = False + return self + + def serve( + self, + netuid: int, + subtensor: Optional["Subtensor"] = None, + certificate: Optional[Certificate] = None, + ) -> "Axon": + """ + Serves the Axon on the specified subtensor connection using the configured wallet. This method + registers the Axon with a specific subnet within the Bittensor network, identified by the ``netuid``. + It links the Axon to the broader network, allowing it to participate in the decentralized exchange + of information. + + Args: + netuid (int): The unique identifier of the subnet to register on. This ID is essential for the Axon to + correctly position itself within the Bittensor network topology. + subtensor (Optional[bittensor.core.subtensor.Subtensor]): The subtensor connection to use for serving. If + not provided, a new connection is established based on default configurations. + + Returns: + bittensor.core.axon.Axon: The Axon instance that is now actively serving on the specified subtensor. + + Example:: + + my_axon = bittensor.Axon(...) + subtensor = bt.subtensor(network="local") # Local by default + my_axon.serve(netuid=1, subtensor=subtensor) # Serves the axon on subnet with netuid 1 + + Note: + The ``serve`` method is crucial for integrating the Axon into the Bittensor network, allowing it + to start receiving and processing requests from other neurons. + """ + if subtensor is not None and hasattr(subtensor, "serve_axon"): + subtensor.serve_axon(netuid=netuid, axon=self, certificate=certificate) + return self + + async def default_verify(self, synapse: "Synapse"): + """ + This method is used to verify the authenticity of a received message using a digital signature. + + It ensures that the message was not tampered with and was sent by the expected sender. + + The :func:`default_verify` method in the Bittensor framework is a critical security function within the + Axon server. It is designed to authenticate incoming messages by verifying their digital + signatures. This verification ensures the integrity of the message and confirms that it was + indeed sent by the claimed sender. The method plays a pivotal role in maintaining the trustworthiness + and reliability of the communication within the Bittensor network. + + Key Features + Security Assurance + The default_verify method is crucial for ensuring the security of the Bittensor network. By verifying + digital signatures, it guards against unauthorized access and data manipulation. + + Preventing Replay Attacks + The method checks for increasing nonce values, which is a vital + step in preventing replay attacks. A replay attack involves an adversary reusing or + delaying the transmission of a valid data transmission to deceive the receiver. + The first time a nonce is seen, it is checked for freshness by ensuring it is + within an acceptable delta time range. + + Authenticity and Integrity Checks + By verifying that the message's digital signature matches + its content, the method ensures the message's authenticity (it comes from the claimed + sender) and integrity (it hasn't been altered during transmission). + + Trust in Communication + This method fosters trust in the network communication. Neurons + (nodes in the Bittensor network) can confidently interact, knowing that the messages they + receive are genuine and have not been tampered with. + + Cryptographic Techniques + The method's reliance on asymmetric encryption techniques is a + cornerstone of modern cryptographic security, ensuring that only entities with the correct + cryptographic keys can participate in secure communication. + + Args: + synapse(bittensor.core.synapse.Synapse): bittensor request synapse. + + Raises: + Exception: If the ``receiver_hotkey`` doesn't match with ``self.receiver_hotkey``. + Exception: If the nonce is not larger than the previous nonce for the same endpoint key. + Exception: If the signature verification fails. + + After successful verification, the nonce for the given endpoint key is updated. + + Note: + The verification process assumes the use of an asymmetric encryption algorithm, + where the sender signs the message with their private key and the receiver verifies the + signature using the sender's public key. + """ + # Build the keypair from the dendrite_hotkey + if synapse.dendrite is not None: + keypair = Keypair(ss58_address=synapse.dendrite.hotkey) + + # Build the signature messages. + message = f"{synapse.dendrite.nonce}.{synapse.dendrite.hotkey}.{self.wallet.hotkey.ss58_address}.{synapse.dendrite.uuid}.{synapse.computed_body_hash}" + + # Build the unique endpoint key. + endpoint_key = f"{synapse.dendrite.hotkey}:{synapse.dendrite.uuid}" + + # Requests must have nonces to be safe from replays + if synapse.dendrite.nonce is None: + raise Exception("Missing Nonce") + + # Newer nonce structure post v7.2 + if ( + synapse.dendrite.version is not None + and synapse.dendrite.version >= V_7_2_0 + ): + # If we don't have a nonce stored, ensure that the nonce falls within + # a reasonable delta. + current_time_ns = time.time_ns() + allowed_window_ns = allowed_nonce_window_ns( + current_time_ns, synapse.timeout + ) + + if ( + self.nonces.get(endpoint_key) is None + and synapse.dendrite.nonce <= allowed_window_ns + ): + diff_seconds, allowed_delta_seconds = calculate_diff_seconds( + current_time_ns, synapse.timeout, synapse.dendrite.nonce + ) + raise Exception( + f"Nonce is too old: acceptable delta is {allowed_delta_seconds:.2f} seconds but request was {diff_seconds:.2f} seconds old" + ) + + # If a nonce is stored, ensure the new nonce + # is greater than the previous nonce + if ( + self.nonces.get(endpoint_key) is not None + and synapse.dendrite.nonce <= self.nonces[endpoint_key] + ): + raise Exception("Nonce is too old, a newer one was last processed") + # Older nonce structure pre v7.2 + else: + if ( + self.nonces.get(endpoint_key) is not None + and synapse.dendrite.nonce <= self.nonces[endpoint_key] + ): + raise Exception("Nonce is too old, a newer one was last processed") + + if synapse.dendrite.signature and not keypair.verify( + message, synapse.dendrite.signature + ): + raise Exception( + f"Signature mismatch with {message} and {synapse.dendrite.signature}" + ) + + # Success + self.nonces[endpoint_key] = synapse.dendrite.nonce # type: ignore + else: + raise SynapseDendriteNoneException(synapse=synapse) + + +def create_error_response(synapse: "Synapse") -> "JSONResponse": + """Creates an error response based on the provided synapse object. + + Args: + synapse (bittensor.core.synapse.Synapse): The synapse object containing details about the request and the + associated axon. + + Returns: + JSONResponse: A JSON response with a status code and content indicating the error message. + """ + if synapse.axon is None: + return JSONResponse( + status_code=400, + headers=synapse.to_headers(), + content={"message": "Invalid request name"}, + ) + else: + return JSONResponse( + status_code=synapse.axon.status_code or 400, + headers=synapse.to_headers(), + content={"message": synapse.axon.status_message}, + ) + + +def log_and_handle_error( + synapse: "Synapse", + exception: Exception, + status_code: Optional[int] = None, + start_time: Optional[float] = None, +) -> "Synapse": + """ + Logs the error and updates the synapse object with the appropriate error details. + + Args: + synapse (bittensor.core.synapse.Synapse): The synapse object to be updated with error information. + exception (Exception): The exception that was raised and needs to be logged and handled. + status_code (Optional[int]): The HTTP status code to be set on the synapse object. Defaults to None. + start_time (Optional[float]): The timestamp marking the start of the processing, used to calculate process time. + Defaults to None. + + Returns: + Synapse: The updated synapse object with error details. + """ + if isinstance(exception, SynapseException): + synapse = exception.synapse or synapse + + logging.trace(f"Forward handled exception: {exception}") + else: + logging.trace(f"Forward exception: {traceback.format_exc()}") + + if synapse.axon is None: + synapse.axon = TerminalInfo() + + # Set the status code of the synapse to the given status code. + error_id = str(uuid.uuid4()) + error_type = exception.__class__.__name__ + + # Log the detailed error message for internal use + logging.error(f"{error_type}#{error_id}: {exception}") + + if not status_code and synapse.axon.status_code != 100: + status_code = synapse.axon.status_code + status_message = synapse.axon.status_message + if isinstance(exception, SynapseException): + if not status_code: + if isinstance(exception, PriorityException): + status_code = 503 + elif isinstance(exception, UnknownSynapseError): + status_code = 404 + elif isinstance(exception, BlacklistedException): + status_code = 403 + elif isinstance(exception, NotVerifiedException): + status_code = 401 + elif isinstance(exception, (InvalidRequestNameError, SynapseParsingError)): + status_code = 400 + else: + status_code = 500 + status_message = status_message or str(exception) + else: + status_code = status_code or 500 + status_message = status_message or f"Internal Server Error #{error_id}" + + # Set a user-friendly error message + synapse.axon.status_code = status_code + synapse.axon.status_message = status_message + + if start_time: + # Calculate the processing time by subtracting the start time from the current time. + synapse.axon.process_time = str(time.time() - start_time) # type: ignore + + return synapse + + +class AxonMiddleware(BaseHTTPMiddleware): + """ + The `AxonMiddleware` class is a key component in the Axon server, responsible for processing all incoming requests. + + It handles the essential tasks of verifying requests, executing blacklist checks, + running priority functions, and managing the logging of messages and errors. Additionally, the class + is responsible for updating the headers of the response and executing the requested functions. + + This middleware acts as an intermediary layer in request handling, ensuring that each request is + processed according to the defined rules and protocols of the Bittensor network. It plays a pivotal + role in maintaining the integrity and security of the network communication. + + Args: + app (FastAPI): An instance of the FastAPI application to which this middleware is attached. + axon (bittensor.core.axon.Axon): The Axon instance that will process the requests. + + The middleware operates by intercepting incoming requests, performing necessary preprocessing + (like verification and priority assessment), executing the request through the Axon's endpoints, and + then handling any postprocessing steps such as response header updating and logging. + """ + + def __init__(self, app: "AxonMiddleware", axon: "Axon"): + """ + Initialize the AxonMiddleware class. + + Args: + app (bittensor.core.axon.AxonMiddleware): An instance of the application where the middleware processor is + used. + axon (bittensor.core.axon.Axon): The axon instance used to process the requests. + """ + super().__init__(app) + self.axon = axon + + async def dispatch( + self, request: "Request", call_next: "RequestResponseEndpoint" + ) -> Response: + """ + Asynchronously processes incoming HTTP requests and returns the corresponding responses. This + method acts as the central processing unit of the AxonMiddleware, handling each step in the + request lifecycle. + + Args: + request (Request): The incoming HTTP request to be processed. + call_next (RequestResponseEndpoint): A callable that processes the request and returns a response. + + Returns: + Response: The HTTP response generated after processing the request. + + This method performs several key functions: + + 1. Request Preprocessing: Sets up Synapse object from request headers and fills necessary information. + 2. Logging: Logs the start of request processing. + 3. Blacklist Checking: Verifies if the request is blacklisted. + 4. Request Verification: Ensures the authenticity and integrity of the request. + 5. Priority Assessment: Evaluates and assigns priority to the request. + 6. Request Execution: Calls the next function in the middleware chain to process the request. + 7. Response Postprocessing: Updates response headers and logs the end of the request processing. + + The method also handles exceptions and errors that might occur during each stage, ensuring that + appropriate responses are returned to the client. + """ + # Records the start time of the request processing. + start_time = time.time() + + try: + # Set up the synapse from its headers. + try: + synapse: "Synapse" = await self.preprocess(request) + except Exception as exc: + if isinstance(exc, SynapseException) and exc.synapse is not None: + synapse = exc.synapse + else: + synapse = Synapse() + raise + + # Logs the start of the request processing + if synapse.dendrite is not None: + logging.trace( + f"axon | <-- | {request.headers.get('content-length', -1)} B | {synapse.name} | {synapse.dendrite.hotkey} | {synapse.dendrite.ip}:{synapse.dendrite.port} | 200 | Success " + ) + else: + logging.trace( + f"axon | <-- | {request.headers.get('content-length', -1)} B | {synapse.name} | None | None | 200 | Success " + ) + + # Call the blacklist function + await self.blacklist(synapse) + + # Call verify and return the verified request + await self.verify(synapse) + + # Call the priority function + await self.priority(synapse) + + # Call the run function + response = await self.run(synapse, call_next, request) + + # Handle errors related to preprocess. + except InvalidRequestNameError as e: + if synapse.axon is None: + synapse.axon = TerminalInfo() + synapse.axon.status_code = 400 + synapse.axon.status_message = str(e) + synapse = log_and_handle_error(synapse, e, start_time=start_time) + response = create_error_response(synapse) + + except SynapseException as e: + synapse = e.synapse or synapse + synapse = log_and_handle_error(synapse, e, start_time=start_time) + response = create_error_response(synapse) + + # Handle all other errors. + except Exception as e: + synapse = log_and_handle_error(synapse, e, start_time=start_time) + response = create_error_response(synapse) + + # Logs the end of request processing and returns the response + finally: + # Log the details of the processed synapse, including total size, name, hotkey, IP, port, + # status code, and status message, using the debug level of the logger. + if synapse.dendrite is not None and synapse.axon is not None: + logging.trace( + f"axon | --> | {response.headers.get('content-length', -1)} B | {synapse.name} | {synapse.dendrite.hotkey} | {synapse.dendrite.ip}:{synapse.dendrite.port} | {synapse.axon.status_code} | {synapse.axon.status_message}" + ) + elif synapse.axon is not None: + logging.trace( + f"axon | --> | {response.headers.get('content-length', -1)} B | {synapse.name} | None | None | {synapse.axon.status_code} | {synapse.axon.status_message}" + ) + else: + logging.trace( + f"axon | --> | {response.headers.get('content-length', -1)} B | {synapse.name} | None | None | 200 | Success " + ) + + # Return the response to the requester. + return response + + async def preprocess(self, request: "Request") -> "Synapse": + """ + Performs the initial processing of the incoming request. This method is responsible for + extracting relevant information from the request and setting up the Synapse object, which + represents the state and context of the request within the Axon server. + + Args: + request (Request): The incoming request to be preprocessed. + + Returns: + bittensor.core.synapse.Synapse: The Synapse object representing the preprocessed state of the request. + + The preprocessing involves: + + 1. Extracting the request name from the URL path. + 2. Creating a Synapse instance from the request headers using the appropriate class type. + 3. Filling in the Axon and Dendrite information into the Synapse object. + 4. Signing the Synapse from the Axon side using the wallet hotkey. + + This method sets the foundation for the subsequent steps in the request handling process, + ensuring that all necessary information is encapsulated within the Synapse object. + """ + # Extracts the request name from the URL path. + try: + request_name = request.url.path.split("/")[1] + except Exception: + raise InvalidRequestNameError( + f"Improperly formatted request. Could not parser request {request.url.path}." + ) + + # Creates a synapse instance from the headers using the appropriate forward class type + # based on the request name obtained from the URL path. + request_synapse = self.axon.forward_class_types.get(request_name) + if request_synapse is None: + raise UnknownSynapseError( + f"Synapse name '{request_name}' not found. Available synapses {list(self.axon.forward_class_types.keys())}" + ) + + try: + synapse = request_synapse.from_headers(request.headers) # type: ignore + except Exception: + raise SynapseParsingError( + f"Improperly formatted request. Could not parse headers {request.headers} into synapse of type {request_name}." + ) + synapse.name = request_name + + # Fills the local axon information into the synapse. + synapse.axon.__dict__.update( + { + "version": int(version_as_int), + "uuid": str(self.axon.uuid), + "nonce": time.time_ns(), + "status_code": 100, + } + ) + + # Fills the dendrite information into the synapse. + synapse.dendrite.__dict__.update( + {"port": int(request.client.port), "ip": str(request.client.host)} # type: ignore + ) + + # Signs the synapse from the axon side using the wallet hotkey. + message = f"{synapse.axon.nonce}.{synapse.dendrite.hotkey}.{synapse.axon.hotkey}.{synapse.axon.uuid}" + synapse.axon.signature = f"0x{self.axon.wallet.hotkey.sign(message).hex()}" + + # Return the setup synapse. + return synapse + + async def verify(self, synapse: "Synapse"): + """ + Verifies the authenticity and integrity of the request. This method ensures that the incoming + request meets the predefined security and validation criteria. + + Args: + synapse (bittensor.core.synapse.Synapse): The Synapse object representing the request. + + Raises: + Exception: If the verification process fails due to unmet criteria or security concerns. + + The verification process involves: + + 1. Retrieving the specific verification function for the request's Synapse type. + 2. Executing the verification function and handling any exceptions that arise. + + Successful verification allows the request to proceed further in the processing pipeline, while + failure results in an appropriate exception being raised. + """ + # Start of the verification process. Verification is the process where we ensure that + # the incoming request is from a trusted source or fulfills certain requirements. + # We get a specific verification function from 'verify_fns' dictionary that corresponds + # to our request's name. Each request name (synapse name) has its unique verification function. + verify_fn = ( + self.axon.verify_fns.get(synapse.name) if synapse.name is not None else None + ) + + # If a verification function exists for the request's name + if verify_fn: + try: + # We attempt to run the verification function using the synapse instance + # created from the request. If this function runs without throwing an exception, + # it means that the verification was successful. + ( + await verify_fn(synapse) + if inspect.iscoroutinefunction(verify_fn) + else verify_fn(synapse) + ) + except Exception as e: + # If there was an exception during the verification process, we log that + # there was a verification exception. + logging.trace(f"Verify exception {str(e)}") + + # Check if the synapse.axon object exists + if synapse.axon is not None: + # We set the status code of the synapse to "401" which denotes an unauthorized access. + synapse.axon.status_code = 401 + else: + # If the synapse.axon object doesn't exist, raise an exception. + raise Exception("Synapse.axon object is None") + + # We raise an exception to stop the process and return the error to the requester. + # The error message includes the original exception message. + raise NotVerifiedException( + f"Not Verified with error: {str(e)}", synapse=synapse + ) + + async def blacklist(self, synapse: "Synapse"): + """ + Checks if the request should be blacklisted. This method ensures that requests from disallowed + sources or with malicious intent are blocked from processing. This can be extremely useful for + preventing spam or other forms of abuse. The blacklist is a list of keys or identifiers that + are prohibited from accessing certain resources. + + Args: + synapse (bittensor.core.synapse.Synapse): The Synapse object representing the request. + + Raises: + Exception: If the request is found in the blacklist. + + The blacklist check involves: + + 1. Retrieving the blacklist checking function for the request's Synapse type. + 2. Executing the check and handling the case where the request is blacklisted. + + If a request is blacklisted, it is blocked, and an exception is raised to halt further processing. + """ + # A blacklist is a list of keys or identifiers + # that are prohibited from accessing certain resources. + # We retrieve the blacklist checking function from the 'blacklist_fns' dictionary + # that corresponds to the request's name (synapse name). + blacklist_fn = ( + self.axon.blacklist_fns.get(synapse.name) + if synapse.name is not None + else None + ) + + # If a blacklist checking function exists for the request's name + if blacklist_fn: + # We execute the blacklist checking function using the synapse instance as input. + # If the function returns True, it means that the key or identifier is blacklisted. + blacklisted, reason = ( + await blacklist_fn(synapse) + if inspect.iscoroutinefunction(blacklist_fn) + else blacklist_fn(synapse) + ) + if blacklisted: + # We log that the key or identifier is blacklisted. + logging.trace(f"Blacklisted: {blacklisted}, {reason}") + + # Check if the synapse.axon object exists + if synapse.axon is not None: + # We set the status code of the synapse to "403" which indicates a forbidden access. + synapse.axon.status_code = 403 + else: + # If the synapse.axon object doesn't exist, raise an exception. + raise Exception("Synapse.axon object is None") + + # We raise an exception to halt the process and return the error message to the requester. + raise BlacklistedException( + f"Forbidden. Key is blacklisted: {reason}.", synapse=synapse + ) + + async def priority(self, synapse: "Synapse"): + """ + Executes the priority function for the request. This method assesses and assigns a priority + level to the request, determining its urgency and importance in the processing queue. + + Args: + synapse (bittensor.core.synapse.Synapse): The Synapse object representing the request. + + Raises: + Exception: If the priority assessment process encounters issues, such as timeouts. + + The priority function plays a crucial role in managing the processing load and ensuring that + critical requests are handled promptly. + """ + # Retrieve the priority function from the 'priority_fns' dictionary that corresponds + # to the request's name (synapse name). + priority_fn = self.axon.priority_fns.get(str(synapse.name), None) + + async def submit_task( + executor: "PriorityThreadPoolExecutor", priority: float + ) -> tuple[float, Any]: + """ + Submits the given priority function to the specified executor for asynchronous execution. + The function will run in the provided executor and return the priority value along with the result. + + Args: + executor (bittensor.core.threadpool.PriorityThreadPoolExecutor): The executor in which the priority + function will be run. + priority (float): The priority function to be executed. + + Returns: + tuple: A tuple containing the priority value and the result of the priority function execution. + """ + loop = asyncio.get_running_loop() + future = loop.run_in_executor(executor, lambda: priority) + result_ = await future + return priority, result_ + + # If a priority function exists for the request's name + if priority_fn: + try: + # Execute the priority function and get the priority value. + priority = ( + await priority_fn(synapse) + if inspect.iscoroutinefunction(priority_fn) + else priority_fn(synapse) + ) + + # Submit the task to the thread pool for execution with the given priority. + # The submit_task function will handle the execution and return the result. + _, result = await submit_task(self.axon.thread_pool, priority) + + except TimeoutError as e: + # If the execution of the priority function exceeds the timeout, + # it raises an exception to handle the timeout error. + logging.trace(f"TimeoutError: {str(e)}") + + # Set the status code of the synapse to 408 which indicates a timeout error. + if synapse.axon is not None: + synapse.axon.status_code = 408 + + # Raise an exception to stop the process and return an appropriate error message to the requester. + raise PriorityException( + f"Response timeout after: {synapse.timeout}s", synapse=synapse + ) + + async def run( + self, + synapse: "Synapse", + call_next: "RequestResponseEndpoint", + request: "Request", + ) -> "Response": + """ + Executes the requested function as part of the request processing pipeline. This method calls + the next function in the middleware chain to process the request and generate a response. + + Args: + synapse (bittensor.core.synapse.Synapse): The Synapse object representing the request. + call_next (RequestResponseEndpoint): The next function in the middleware chain to process requests. + request (Request): The original HTTP request. + + Returns: + Response: The HTTP response generated by processing the request. + + This method is a critical part of the request lifecycle, where the actual processing of the + request takes place, leading to the generation of a response. + """ + assert isinstance(synapse, Synapse) + + try: + # The requested function is executed by calling the 'call_next' function, + # passing the original request as an argument. This function processes the request + # and returns the response. + response = await call_next(request) + + except Exception as e: + # Log the exception for debugging purposes. + logging.trace(f"Run exception: {str(e)}") + raise + + # Return the starlet response + return response + + @classmethod + async def synapse_to_response( + cls, + synapse: "Synapse", + start_time: float, + *, + response_override: Optional["Response"] = None, + ) -> "Response": + """ + Converts the Synapse object into a JSON response with HTTP headers. + + Args: + synapse (bittensor.core.synapse.Synapse): The Synapse object representing the request. + start_time (float): The timestamp when the request processing started. + response_override: Instead of serializing the synapse, mutate the provided response object. This is only + really useful for StreamingSynapse responses. + + Returns: + Response: The final HTTP response, with updated headers, ready to be sent back to the client. + + Postprocessing is the last step in the request handling process, ensuring that the response is + properly formatted and contains all necessary information. + """ + if synapse.axon is None: + synapse.axon = TerminalInfo() + + if synapse.axon.status_code is None: + synapse.axon.status_code = 200 + + if synapse.axon.status_code == 200 and not synapse.axon.status_message: + synapse.axon.status_message = "Success" + + synapse.axon.process_time = time.time() - start_time + + if response_override: + response = response_override + else: + serialized_synapse = await serialize_response(response_content=synapse) + response = JSONResponse( + status_code=synapse.axon.status_code, + content=serialized_synapse, + ) + + try: + updated_headers = synapse.to_headers() + except Exception as e: + raise PostProcessException( + f"Error while parsing response headers. Postprocess exception: {str(e)}.", + synapse=synapse, + ) from e + + try: + response.headers.update(updated_headers) + except Exception as e: + raise PostProcessException( + f"Error while updating response headers. Postprocess exception: {str(e)}.", + synapse=synapse, + ) from e + + return response diff --git a/bittensor/core/chain_data/__init__.py b/bittensor/core/chain_data/__init__.py new file mode 100644 index 0000000000..9e1852e1d5 --- /dev/null +++ b/bittensor/core/chain_data/__init__.py @@ -0,0 +1,63 @@ +""" +This module provides data structures and functions for working with the Bittensor network, including neuron and subnet +information, SCALE encoding/decoding, and custom RPC type registry. +""" + +from scalecodec.types import GenericCall + +from .axon_info import AxonInfo +from .chain_identity import ChainIdentity +from .delegate_info import DelegateInfo, DelegatedInfo +from .delegate_info_lite import DelegateInfoLite +from .dynamic_info import DynamicInfo +from .ip_info import IPInfo +from .metagraph_info import ( + MetagraphInfo, + MetagraphInfoEmissions, + MetagraphInfoPool, + MetagraphInfoParams, + SelectiveMetagraphIndex, +) +from .neuron_info import NeuronInfo +from .neuron_info_lite import NeuronInfoLite +from .prometheus_info import PrometheusInfo +from .proposal_vote_data import ProposalVoteData +from .scheduled_coldkey_swap_info import ScheduledColdkeySwapInfo +from .stake_info import StakeInfo +from .subnet_hyperparameters import SubnetHyperparameters +from .subnet_identity import SubnetIdentity +from .subnet_info import SubnetInfo +from .subnet_state import SubnetState +from .weight_commit_info import WeightCommitInfo +from .utils import decode_account_id, process_stake_data + +ProposalCallData = GenericCall + +__all__ = [ + AxonInfo, + ChainIdentity, + DelegateInfo, + DelegatedInfo, + DelegateInfoLite, + DynamicInfo, + IPInfo, + MetagraphInfo, + MetagraphInfoEmissions, + MetagraphInfoParams, + MetagraphInfoPool, + NeuronInfo, + NeuronInfoLite, + PrometheusInfo, + ProposalCallData, + ProposalVoteData, + ScheduledColdkeySwapInfo, + SelectiveMetagraphIndex, + StakeInfo, + SubnetHyperparameters, + SubnetIdentity, + SubnetInfo, + SubnetState, + WeightCommitInfo, + decode_account_id, + process_stake_data, +] diff --git a/bittensor/core/chain_data/axon_info.py b/bittensor/core/chain_data/axon_info.py new file mode 100644 index 0000000000..7b9e666bc2 --- /dev/null +++ b/bittensor/core/chain_data/axon_info.py @@ -0,0 +1,164 @@ +""" +This module defines the `AxonInfo` class, a data structure used to represent information about an axon endpoint +in the bittensor network. +""" + +from dataclasses import asdict, dataclass +from typing import Any, Union + +import netaddr +from async_substrate_interface.utils import json +from bittensor.core.chain_data.info_base import InfoBase +from bittensor.utils import networking +from bittensor.utils.btlogging import logging +from bittensor.utils.registration import torch, use_torch + + +@dataclass +class AxonInfo(InfoBase): + """ + The `AxonInfo` class represents information about an axon endpoint in the bittensor network. This includes + properties such as IP address, ports, and relevant keys. + + Attributes: + version (int): The version of the axon endpoint. + ip (str): The IP address of the axon endpoint. + port (int): The port number the axon endpoint uses. + ip_type (int): The type of IP protocol (e.g., IPv4 or IPv6). + hotkey (str): The hotkey associated with the axon endpoint. + coldkey (str): The coldkey associated with the axon endpoint. + protocol (int): The protocol version (default is 4). + placeholder1 (int): Reserved field (default is 0). + placeholder2 (int): Reserved field (default is 0). + """ + + version: int + ip: str + port: int + ip_type: int + hotkey: str + coldkey: str + protocol: int = 4 + placeholder1: int = 0 + placeholder2: int = 0 + + @property + def is_serving(self) -> bool: + """True if the endpoint is serving.""" + return self.ip != "0.0.0.0" + + def ip_str(self) -> str: + """Return the whole IP as string""" + return networking.ip__str__(self.ip_type, self.ip, self.port) + + def __eq__(self, other: "AxonInfo"): + if other is None: + return False + + if ( + self.version == other.version + and self.ip == other.ip + and self.port == other.port + and self.ip_type == other.ip_type + and self.coldkey == other.coldkey + and self.hotkey == other.hotkey + ): + return True + + return False + + def __str__(self): + return f"AxonInfo( {self.ip_str()}, {self.hotkey}, {self.coldkey}, {self.version} )" + + def __repr__(self): + return self.__str__() + + def to_string(self) -> str: + """Converts the `AxonInfo` object to a string representation using JSON.""" + try: + return json.dumps(asdict(self)) + except (TypeError, ValueError) as e: + logging.error(f"Error converting AxonInfo to string: {e}") + return AxonInfo(0, "", 0, 0, "", "").to_string() + + @classmethod + def _from_dict(cls, data): + """Returns a AxonInfo object from decoded chain data.""" + return AxonInfo( + version=data["version"], + ip=str(netaddr.IPAddress(data["ip"])), + port=data["port"], + ip_type=data["ip_type"], + placeholder1=data["placeholder1"], + placeholder2=data["placeholder2"], + protocol=data["protocol"], + hotkey=data["hotkey"], + coldkey=data["coldkey"], + ) + + @classmethod + def from_string(cls, json_string: str) -> "AxonInfo": + """ + Creates an `AxonInfo` object from its string representation using JSON. + + Args: + json_string (str): The JSON string representation of the AxonInfo object. + + Returns: + AxonInfo: An instance of AxonInfo created from the JSON string. If decoding fails, returns a default + `AxonInfo` object with default values. + + Raises: + json.JSONDecodeError: If there is an error in decoding the JSON string. + TypeError: If there is a type error when creating the AxonInfo object. + ValueError: If there is a value error when creating the AxonInfo object. + """ + try: + data = json.loads(json_string) + return cls(**data) + except json.JSONDecodeError as e: + logging.error(f"Error decoding JSON: {e}") + except TypeError as e: + logging.error(f"Type error: {e}") + except ValueError as e: + logging.error(f"Value error: {e}") + return AxonInfo(0, "", 0, 0, "", "") + + @classmethod + def from_neuron_info(cls, neuron_info: dict) -> "AxonInfo": + """ + Converts a dictionary to an `AxonInfo` object. + + Args: + neuron_info (dict): A dictionary containing the neuron information. + + Returns: + instance (AxonInfo): An instance of AxonInfo created from the dictionary. + """ + return cls( + version=neuron_info["axon_info"]["version"], + ip=networking.int_to_ip(int(neuron_info["axon_info"]["ip"])), + port=neuron_info["axon_info"]["port"], + ip_type=neuron_info["axon_info"]["ip_type"], + hotkey=neuron_info["hotkey"], + coldkey=neuron_info["coldkey"], + ) + + def to_parameter_dict( + self, + ) -> Union[dict[str, Union[int, str]], "torch.nn.ParameterDict"]: + """Returns a torch tensor or dict of the subnet info, depending on the USE_TORCH flag set.""" + if use_torch(): + return torch.nn.ParameterDict(self.__dict__) + else: + return self.__dict__ + + @classmethod + def from_parameter_dict( + cls, parameter_dict: Union[dict[str, Any], "torch.nn.ParameterDict"] + ) -> "AxonInfo": + """Returns an axon_info object from a torch parameter_dict or a parameter dict.""" + if use_torch(): + return cls(**dict(parameter_dict)) + else: + return cls(**parameter_dict) diff --git a/bittensor/core/chain_data/chain_identity.py b/bittensor/core/chain_data/chain_identity.py new file mode 100644 index 0000000000..7514bde5f0 --- /dev/null +++ b/bittensor/core/chain_data/chain_identity.py @@ -0,0 +1,28 @@ +from dataclasses import dataclass +from bittensor.core.chain_data.info_base import InfoBase + + +@dataclass +class ChainIdentity(InfoBase): + """Dataclass for chain identity information.""" + + name: str + url: str + github: str + image: str + discord: str + description: str + additional: str + + @classmethod + def _from_dict(cls, decoded: dict) -> "ChainIdentity": + """Returns a ChainIdentity object from decoded chain data.""" + return cls( + name=decoded["name"], + url=decoded["url"], + github=decoded["github_repo"], + image=decoded["image"], + discord=decoded["discord"], + description=decoded["description"], + additional=decoded["additional"], + ) diff --git a/bittensor/core/chain_data/delegate_info.py b/bittensor/core/chain_data/delegate_info.py new file mode 100644 index 0000000000..c0a595bf39 --- /dev/null +++ b/bittensor/core/chain_data/delegate_info.py @@ -0,0 +1,117 @@ +from dataclasses import dataclass +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 + + +@dataclass +class DelegateInfoBase(InfoBase): + """Base class containing common delegate information fields. + + Attributes: + hotkey_ss58 (str): Hotkey of delegate. + owner_ss58 (str): Coldkey of owner. + take (float): Take of the delegate as a percentage. + validator_permits (list[int]): List of subnets that the delegate is allowed to validate on. + registrations (list[int]): List of subnets that the delegate is registered on. + return_per_1000 (Balance): Return per 1000 tao of the delegate over a day. + total_daily_return (Balance): Total daily return of the delegate. + """ + + hotkey_ss58: str # Hotkey of delegate + owner_ss58: str # Coldkey of owner + take: float # Take of the delegate as a percentage + validator_permits: list[ + int + ] # List of subnets that the delegate is allowed to validate on + registrations: list[int] # list of subnets that the delegate is registered on + return_per_1000: Balance # Return per 1000 tao of the delegate over a day + total_daily_return: Balance # Total daily return of the delegate + + +@dataclass +class DelegateInfo(DelegateInfoBase): + """ + Dataclass for delegate information. + + Additional Attributes: + total_stake (dict[int, Balance]): Total stake of the delegate mapped by netuid. + nominators (dict[str, dict[int, Balance]]): Mapping of nominator SS58 addresses to their stakes per subnet. + """ + + total_stake: dict[int, Balance] # Total stake of the delegate by netuid and stake + nominators: dict[ + str, dict[int, Balance] + ] # Mapping of nominator addresses to their stakes per subnet + + @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")) + + nominators = {} + total_stake_by_netuid = {} + + for raw_nominator, raw_stakes in decoded.get("nominators", []): + nominator_ss58 = decode_account_id(raw_nominator) + stakes = { + int(netuid): Balance.from_rao(stake_amt).set_unit(int(netuid)) + for (netuid, stake_amt) in raw_stakes + } + nominators[nominator_ss58] = stakes + + for netuid, stake in stakes.items(): + if netuid not in total_stake_by_netuid: + total_stake_by_netuid[netuid] = Balance(0).set_unit(netuid) + total_stake_by_netuid[netuid] += stake + + return cls( + hotkey_ss58=hotkey, + total_stake=total_stake_by_netuid, + nominators=nominators, + owner_ss58=owner, + take=u16_normalized_float(decoded.get("take")), + validator_permits=list(decoded.get("validator_permits", [])), + registrations=list(decoded.get("registrations", [])), + return_per_1000=Balance.from_rao(decoded.get("return_per_1000")), + total_daily_return=Balance.from_rao(decoded.get("total_daily_return")), + ) + + +@dataclass +class DelegatedInfo(DelegateInfoBase): + """ + Dataclass for delegated information. This class represents a delegate's information + specific to a particular subnet. + + Additional Attributes: + netuid (int): Network ID of the subnet. + stake (Balance): Stake amount for this specific delegation. + """ + + netuid: int + stake: Balance + + @classmethod + 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")) + return cls( + hotkey_ss58=hotkey, + owner_ss58=owner, + take=u16_normalized_float(delegate_info.get("take")), + validator_permits=list(delegate_info.get("validator_permits", [])), + registrations=list(delegate_info.get("registrations", [])), + return_per_1000=Balance.from_rao(delegate_info.get("return_per_1000")), + total_daily_return=Balance.from_rao( + delegate_info.get("total_daily_return") + ), + netuid=int(netuid), + stake=Balance.from_rao(int(stake)).set_unit(int(netuid)), + ) diff --git a/bittensor/core/chain_data/delegate_info_lite.py b/bittensor/core/chain_data/delegate_info_lite.py new file mode 100644 index 0000000000..1138696314 --- /dev/null +++ b/bittensor/core/chain_data/delegate_info_lite.py @@ -0,0 +1,47 @@ +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 + + +@dataclass +class DelegateInfoLite(InfoBase): + """ + Dataclass for `DelegateLiteInfo`. This is a lighter version of :func:``DelegateInfo``. + + Args: + delegate_ss58 (str): Hotkey of the delegate for which the information is being fetched. + take (float): Take of the delegate as a percentage. + nominators (int): Count of the nominators of the delegate. + owner_ss58 (str): Coldkey of the owner. + registrations (list[int]): List of subnets that the delegate is registered on. + validator_permits (list[int]): List of subnets that the delegate is allowed to validate on. + return_per_1000 (int): Return per 1000 TAO, for the delegate over a day. + total_daily_return (int): Total daily return of the delegate. + """ + + delegate_ss58: str # Hotkey of delegate + take: float # Take of the delegate as a percentage + nominators: int # Count of the nominators of the delegate. + owner_ss58: str # Coldkey of owner + registrations: list[int] # List of subnets that the delegate is registered on + validator_permits: list[ + int + ] # List of subnets that the delegate is allowed to validate on + return_per_1000: Balance # Return per 1000 tao for the delegate over a day + total_daily_return: Balance # Total daily return of the delegate + + @classmethod + def _from_dict(cls, decoded: dict) -> "DelegateInfoLite": + return DelegateInfoLite( + delegate_ss58=decode_account_id(decoded["delegate_ss58"]), + take=u16_normalized_float(decoded["take"]), + nominators=decoded["nominators"], + owner_ss58=decode_account_id(decoded["owner_ss58"]), + registrations=decoded["registrations"], + validator_permits=decoded["validator_permits"], + return_per_1000=Balance.from_rao(decoded["return_per_1000"]), + total_daily_return=Balance.from_rao(decoded["total_daily_return"]), + ) diff --git a/bittensor/core/chain_data/dynamic_info.py b/bittensor/core/chain_data/dynamic_info.py new file mode 100644 index 0000000000..416dfd29c4 --- /dev/null +++ b/bittensor/core/chain_data/dynamic_info.py @@ -0,0 +1,245 @@ +""" +This module defines the `DynamicInfo` data class and associated methods for handling and decoding +dynamic information in the Bittensor network. +""" + +from dataclasses import dataclass +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 + + +@dataclass +class DynamicInfo(InfoBase): + netuid: int + owner_hotkey: str + owner_coldkey: str + subnet_name: str + symbol: str + tempo: int + last_step: int + blocks_since_last_step: int + emission: Balance + alpha_in: Balance + alpha_out: Balance + tao_in: Balance + price: Optional[Balance] + k: float + is_dynamic: bool + alpha_out_emission: Balance + alpha_in_emission: Balance + tao_in_emission: Balance + pending_alpha_emission: Balance + pending_root_emission: Balance + network_registered_at: int + subnet_volume: Balance + subnet_identity: Optional[SubnetIdentity] + moving_price: float + + @classmethod + def _from_dict(cls, decoded: dict) -> "DynamicInfo": + """Returns a DynamicInfo object from decoded chain data.""" + + netuid = int(decoded["netuid"]) + symbol = bytes([int(b) for b in decoded["token_symbol"]]).decode() + subnet_name = bytes([int(b) for b in decoded["subnet_name"]]).decode() + + is_dynamic = ( + 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"]) + + emission = Balance.from_rao(decoded["emission"]).set_unit(0) + alpha_in = Balance.from_rao(decoded["alpha_in"]).set_unit(netuid) + alpha_out = Balance.from_rao(decoded["alpha_out"]).set_unit(netuid) + tao_in = Balance.from_rao(decoded["tao_in"]).set_unit(0) + alpha_out_emission = Balance.from_rao(decoded["alpha_out_emission"]).set_unit( + netuid + ) + alpha_in_emission = Balance.from_rao(decoded["alpha_in_emission"]).set_unit( + netuid + ) + tao_in_emission = Balance.from_rao(decoded["tao_in_emission"]).set_unit(0) + pending_alpha_emission = Balance.from_rao( + decoded["pending_alpha_emission"] + ).set_unit(netuid) + pending_root_emission = Balance.from_rao( + decoded["pending_root_emission"] + ).set_unit(0) + + subnet_volume = Balance.from_rao(decoded["subnet_volume"]).set_unit(netuid) + + 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(), + ) + else: + subnet_identity = None + + price = decoded.get("price", None) + + if price and not isinstance(price, Balance): + raise ValueError(f"price must be a Balance object, got {type(price)}.") + + return cls( + netuid=netuid, + owner_hotkey=owner_hotkey, + owner_coldkey=owner_coldkey, + subnet_name=subnet_name, + symbol=symbol, + tempo=int(decoded["tempo"]), + last_step=int(decoded["last_step"]), + blocks_since_last_step=int(decoded["blocks_since_last_step"]), + emission=emission, + alpha_in=alpha_in, + alpha_out=alpha_out, + tao_in=tao_in, + k=tao_in.rao * alpha_in.rao, + is_dynamic=is_dynamic, + price=( + price + if price is not None + else Balance.from_tao(tao_in.tao / alpha_in.tao).set_unit(netuid) + ), + alpha_out_emission=alpha_out_emission, + alpha_in_emission=alpha_in_emission, + tao_in_emission=tao_in_emission, + pending_alpha_emission=pending_alpha_emission, + pending_root_emission=pending_root_emission, + network_registered_at=int(decoded["network_registered_at"]), + subnet_identity=subnet_identity, + subnet_volume=subnet_volume, + moving_price=fixed_to_float(decoded["moving_price"], 32), + ) + + def tao_to_alpha(self, tao: Union[Balance, float, int]) -> Balance: + if isinstance(tao, (float, int)): + tao = Balance.from_tao(tao) + if self.price.tao != 0: + return Balance.from_tao(tao.tao / self.price.tao).set_unit(self.netuid) + else: + return Balance.from_tao(0) + + def alpha_to_tao(self, alpha: Union[Balance, float, int]) -> Balance: + if isinstance(alpha, (float, int)): + alpha = Balance.from_tao(alpha) + return Balance.from_tao(alpha.tao * self.price.tao) + + def tao_to_alpha_with_slippage( + self, tao: Union[Balance, float, int], percentage: bool = False + ) -> Union[tuple[Balance, Balance], float]: + """ + Returns an estimate of how much Alpha would a staker receive if they stake their tao using the current pool state. + + Arguments: + tao: Amount of TAO to stake. + percentage: percentage + + Returns: + If percentage is False, a tuple of balances where the first part is the amount of Alpha received, and the + second part (slippage) is the difference between the estimated amount and ideal + amount as if there was no slippage. If percentage is True, a float representing the slippage percentage. + """ + if isinstance(tao, (float, int)): + tao = Balance.from_tao(tao) + + if self.is_dynamic: + new_tao_in = self.tao_in + tao + if new_tao_in == 0: + return tao, Balance.from_rao(0) + new_alpha_in = self.k / new_tao_in + + # Amount of alpha given to the staker + alpha_returned = Balance.from_rao( + self.alpha_in.rao - new_alpha_in.rao + ).set_unit(self.netuid) + + # Ideal conversion as if there is no slippage, just price + alpha_ideal = self.tao_to_alpha(tao) + + if alpha_ideal.tao > alpha_returned.tao: + slippage = Balance.from_tao( + alpha_ideal.tao - alpha_returned.tao + ).set_unit(self.netuid) + else: + slippage = Balance.from_tao(0) + else: + alpha_returned = tao.set_unit(self.netuid) + slippage = Balance.from_tao(0) + + if percentage: + slippage_pct_float = ( + 100 * float(slippage) / float(slippage + alpha_returned) + if slippage + alpha_returned != 0 + else 0 + ) + return slippage_pct_float + else: + return alpha_returned, slippage + + slippage = tao_to_alpha_with_slippage + tao_slippage = tao_to_alpha_with_slippage + + def alpha_to_tao_with_slippage( + self, alpha: Union[Balance, float, int], percentage: bool = False + ) -> Union[tuple[Balance, Balance], float]: + """ + Returns an estimate of how much TAO would a staker receive if they unstake their alpha using the current pool state. + + Args: + alpha: Amount of Alpha to stake. + percentage: percentage + + Returns: + If percentage is False, a tuple of balances where the first part is the amount of TAO received, and the + second part (slippage) is the difference between the estimated amount and ideal + amount as if there was no slippage. If percentage is True, a float representing the slippage percentage. + """ + if isinstance(alpha, (float, int)): + alpha = Balance.from_tao(alpha) + + if self.is_dynamic: + new_alpha_in = self.alpha_in + alpha + new_tao_reserve = self.k / new_alpha_in + # Amount of TAO given to the unstaker + tao_returned = Balance.from_rao(self.tao_in.rao - new_tao_reserve.rao) + + # Ideal conversion as if there is no slippage, just price + tao_ideal = self.alpha_to_tao(alpha) + + if tao_ideal > tao_returned: + slippage = Balance.from_tao(tao_ideal.tao - tao_returned.tao) + else: + slippage = Balance.from_tao(0) + else: + tao_returned = alpha.set_unit(0) + slippage = Balance.from_tao(0) + + if percentage: + slippage_pct_float = ( + 100 * float(slippage) / float(slippage + tao_returned) + if slippage + tao_returned != 0 + else 0 + ) + return slippage_pct_float + else: + return tao_returned, slippage + + alpha_slippage = alpha_to_tao_with_slippage diff --git a/bittensor/core/chain_data/info_base.py b/bittensor/core/chain_data/info_base.py new file mode 100644 index 0000000000..e2718619af --- /dev/null +++ b/bittensor/core/chain_data/info_base.py @@ -0,0 +1,30 @@ +from dataclasses import dataclass +from typing import Any, TypeVar + +from bittensor.core.errors import SubstrateRequestException + +# NOTE: once Python 3.10+ is required, we can use `typing.Self` instead of this for better ide integration and type hinting. +# This current generic does not play so nice with the inherited type hinting. +T = TypeVar("T", bound="InfoBase") + + +@dataclass +class InfoBase: + """Base dataclass for info objects.""" + + @classmethod + def from_dict(cls, decoded: dict) -> T: + try: + return cls._from_dict(decoded) + except KeyError as e: + raise SubstrateRequestException( + f"The {cls} structure is missing {e} from the chain.", + ) + + @classmethod + def list_from_dicts(cls, any_list: list[Any]) -> list[T]: + return [cls.from_dict(any_) for any_ in any_list] + + @classmethod + def _from_dict(cls, decoded: dict) -> T: + return cls(**decoded) diff --git a/bittensor/core/chain_data/ip_info.py b/bittensor/core/chain_data/ip_info.py new file mode 100644 index 0000000000..02cde8f905 --- /dev/null +++ b/bittensor/core/chain_data/ip_info.py @@ -0,0 +1,58 @@ +from dataclasses import dataclass +from typing import Any, Union + +from bittensor.utils import networking as net +from bittensor.utils.registration import torch, use_torch + + +@dataclass +class IPInfo: + """ + Dataclass representing IP information. + + Attributes: + ip (str): The IP address as a string. + ip_type (int): The type of the IP address (e.g., IPv4, IPv6). + protocol (int): The protocol associated with the IP (e.g., TCP, UDP). + """ + + ip: str + ip_type: int + protocol: int + + def encode(self) -> dict[str, Any]: + """Returns a dictionary of the IPInfo object that can be encoded.""" + return { + "ip": net.ip_to_int( + self.ip + ), # IP type and protocol are encoded together as a u8 + "ip_type_and_protocol": ((self.ip_type << 4) + self.protocol) & 0xFF, + } + + @classmethod + def _from_dict(cls, decoded: dict) -> "IPInfo": + """Returns a IPInfo object from decoded chain data.""" + return IPInfo( + ip_type=decoded["ip_type_and_protocol"] >> 4, + ip=net.int_to_ip(decoded["ip"]), + protocol=decoded["ip_type_and_protocol"] & 0xF, + ) + + def to_parameter_dict( + self, + ) -> Union[dict[str, Union[str, int]], "torch.nn.ParameterDict"]: + """Returns a torch tensor or dict of the subnet IP info.""" + if use_torch(): + return torch.nn.ParameterDict(self.__dict__) + else: + return self.__dict__ + + @classmethod + def from_parameter_dict( + cls, parameter_dict: Union[dict[str, Any], "torch.nn.ParameterDict"] + ) -> "IPInfo": + """Creates a IPInfo instance from a parameter dictionary.""" + if use_torch(): + return cls(**dict(parameter_dict)) + else: + return cls(**parameter_dict) diff --git a/bittensor/core/chain_data/metagraph_info.py b/bittensor/core/chain_data/metagraph_info.py new file mode 100644 index 0000000000..cc329a84ad --- /dev/null +++ b/bittensor/core/chain_data/metagraph_info.py @@ -0,0 +1,514 @@ +from enum import Enum + +from dataclasses import dataclass +from typing import Optional, Union + +from bittensor.core import settings +from bittensor.core.chain_data.axon_info import AxonInfo +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 u64_normalized_float as u64tf, u16_normalized_float as u16tf +from bittensor.utils.balance import Balance, fixed_to_float + + +# to balance with unit (shortcut) +def _tbwu(val: Optional[int], netuid: Optional[int] = 0) -> Optional[Balance]: + """Returns a Balance object from a value and unit.""" + if val is None: + return None + return Balance.from_rao(val, netuid) + + +def _chr_str(codes: tuple[int]) -> str: + """Converts a tuple of integer Unicode code points into a string.""" + return "".join(map(chr, codes)) + + +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.""" + if isinstance(data, (list, tuple)): + if len(data) > 0: + return [ + {k: chr_transform(v) for k, v in item.items()} + if item is not None + else None + for item in data + ] + return {} + elif isinstance(data, dict): + return {k: chr_transform(v) for k, v in data.items()} + return None + + +@dataclass +class MetagraphInfo(InfoBase): + # Subnet index + netuid: int + + # Name and symbol + name: str + symbol: str + identity: Optional[SubnetIdentity] + network_registered_at: int + + # Keys for owner. + owner_hotkey: Optional[str] # hotkey + owner_coldkey: Optional[str] # coldkey + + # Tempo terms. + block: int # block at call. + tempo: int # epoch tempo + last_step: int + blocks_since_last_step: int + + # Subnet emission terms + subnet_emission: Balance # subnet emission via tao + alpha_in: Balance # amount of alpha in reserve + alpha_out: Balance # amount of alpha outstanding + tao_in: Balance # amount of tao injected per block + alpha_out_emission: Balance # amount injected in alpha reserves per block + alpha_in_emission: Balance # amount injected outstanding per block + tao_in_emission: Balance # amount of tao injected per block + pending_alpha_emission: Balance # pending alpha to be distributed + pending_root_emission: Balance # pending tao for root divs to be distributed + subnet_volume: Balance # volume of the subnet in TAO + moving_price: Balance # subnet moving price. + + # Hparams for epoch + rho: int # subnet rho param + kappa: float # subnet kappa param + + # Validator params + min_allowed_weights: float # min allowed weights per val + max_weights_limit: float # max allowed weights per val + weights_version: int # allowed weights version + weights_rate_limit: int # rate limit on weights. + activity_cutoff: int # validator weights cut off period in blocks + max_validators: int # max allowed validators. + + # Registration + num_uids: int + max_uids: int + burn: Balance # current burn cost. + difficulty: float # current difficulty. + registration_allowed: bool # allows registrations. + pow_registration_allowed: bool # pow registration enabled. + immunity_period: int # subnet miner immunity period + min_difficulty: float # min pow difficulty + max_difficulty: float # max pow difficulty + min_burn: Balance # min tao burn + max_burn: Balance # max tao burn + adjustment_alpha: float # adjustment speed for registration params. + adjustment_interval: int # pow and burn adjustment interval + target_regs_per_interval: int # target registrations per interval + max_regs_per_block: int # max registrations per block. + serving_rate_limit: int # axon serving rate limit + + # CR + commit_reveal_weights_enabled: bool # Is CR enabled. + commit_reveal_period: int # Commit reveal interval + + # Bonds + liquid_alpha_enabled: bool # Bonds liquid enabled. + alpha_high: float # Alpha param high + alpha_low: float # Alpha param low + bonds_moving_avg: float # Bonds moving avg + + # Metagraph info. + hotkeys: list[str] # hotkey per UID + coldkeys: list[str] # coldkey per UID + identities: list[Optional[ChainIdentity]] # coldkeys identities + axons: list[AxonInfo] # UID axons. + active: list[bool] # Active per UID + validator_permit: list[bool] # Val permit per UID + pruning_score: list[float] # Pruning per UID + last_update: list[int] # Last update per UID + emission: list[Balance] # Emission per UID + dividends: list[float] # Dividends per UID + incentives: list[float] # Mining incentives per UID + consensus: list[float] # Consensus per UID + trust: list[float] # Trust per UID + rank: list[float] # Rank per UID + block_at_registration: list[int] # Reg block per UID + alpha_stake: list[Balance] # Alpha staked per UID + tao_stake: list[Balance] # TAO staked per UID + total_stake: list[Balance] # Total stake per UID + + # Dividend break down. + tao_dividends_per_hotkey: list[ + tuple[str, Balance] + ] # List of dividend payouts in tao via root. + alpha_dividends_per_hotkey: list[ + tuple[str, Balance] + ] # List of dividend payout in alpha via subnet. + + # List of validators + validators: list[str] + + @classmethod + def _from_dict(cls, decoded: dict) -> "MetagraphInfo": + """Returns a MetagraphInfo object from decoded chain data.""" + # Subnet index + _netuid = decoded["netuid"] + + # Name and symbol + if name := decoded.get("name"): + decoded.update({"name": bytes(name).decode()}) + + if symbol := decoded.get("symbol"): + decoded.update({"symbol": bytes(symbol).decode()}) + + ii_list = [] + if decoded.get("identity") is not None: + ii_list.append("identity") + + 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, + # Name and symbol + name=decoded["name"], + symbol=decoded["symbol"], + 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 + ), + # Tempo terms. + block=decoded["block"], + tempo=decoded["tempo"], + last_step=decoded["last_step"], + blocks_since_last_step=decoded["blocks_since_last_step"], + # Subnet emission terms + subnet_emission=_tbwu(decoded["subnet_emission"]), + alpha_in=_tbwu(decoded["alpha_in"], _netuid), + alpha_out=_tbwu(decoded["alpha_out"], _netuid), + tao_in=_tbwu(decoded["tao_in"]), + alpha_out_emission=_tbwu(decoded["alpha_out_emission"], _netuid), + alpha_in_emission=_tbwu(decoded["alpha_in_emission"], _netuid), + tao_in_emission=_tbwu(decoded["tao_in_emission"]), + pending_alpha_emission=_tbwu(decoded["pending_alpha_emission"], _netuid), + pending_root_emission=_tbwu(decoded["pending_root_emission"]), + subnet_volume=_tbwu(decoded["subnet_volume"], _netuid), + moving_price=( + Balance.from_tao(fixed_to_float(decoded.get("moving_price"), 32)) + if decoded.get("moving_price") is not None + else None + ), + # Hparams for epoch + rho=decoded["rho"], + kappa=decoded["kappa"], + # Validator params + min_allowed_weights=( + u16tf(decoded["min_allowed_weights"]) + if decoded.get("min_allowed_weights") is not None + else None + ), + max_weights_limit=( + u16tf(decoded["max_weights_limit"]) + if decoded["max_weights_limit"] is not None + else None + ), + weights_version=decoded["weights_version"], + weights_rate_limit=decoded["weights_rate_limit"], + activity_cutoff=decoded["activity_cutoff"], + max_validators=decoded["max_validators"], + # Registration + num_uids=decoded["num_uids"], + max_uids=decoded["max_uids"], + burn=_tbwu(decoded["burn"]), + difficulty=( + u64tf(decoded["difficulty"]) + if decoded["difficulty"] is not None + else None + ), + registration_allowed=decoded["registration_allowed"], + pow_registration_allowed=decoded["pow_registration_allowed"], + immunity_period=decoded["immunity_period"], + min_difficulty=( + u64tf(decoded["min_difficulty"]) + if decoded["min_difficulty"] is not None + else None + ), + max_difficulty=( + u64tf(decoded["max_difficulty"]) + if decoded["max_difficulty"] is not None + else None + ), + min_burn=_tbwu(decoded["min_burn"]), + max_burn=_tbwu(decoded["max_burn"]), + adjustment_alpha=( + u64tf(decoded["adjustment_alpha"]) + if decoded["adjustment_alpha"] is not None + else None + ), + adjustment_interval=decoded["adjustment_interval"], + target_regs_per_interval=decoded["target_regs_per_interval"], + max_regs_per_block=decoded["max_regs_per_block"], + serving_rate_limit=decoded["serving_rate_limit"], + # CR + commit_reveal_weights_enabled=decoded["commit_reveal_weights_enabled"], + commit_reveal_period=decoded["commit_reveal_period"], + # Bonds + liquid_alpha_enabled=decoded["liquid_alpha_enabled"], + alpha_high=( + u16tf(decoded["alpha_high"]) + if decoded["alpha_high"] is not None + else None + ), + alpha_low=( + u16tf(decoded["alpha_low"]) + if decoded["alpha_low"] is not None + else None + ), + bonds_moving_avg=( + u64tf(decoded["bonds_moving_avg"]) + if decoded["bonds_moving_avg"] is not None + 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 + ), + identities=decoded["identities"], + axons=decoded.get("axons", []), + active=decoded["active"], + validator_permit=decoded["validator_permit"], + pruning_score=( + [u16tf(ps) for ps in decoded.get("pruning_score", [])] + if decoded.get("pruning_score") is not None + else None + ), + last_update=decoded["last_update"], + emission=( + [_tbwu(em, _netuid) for em in decoded.get("emission", [])] + if decoded.get("emission") is not None + else None + ), + dividends=( + [u16tf(dv) for dv in decoded.get("dividends", [])] + if decoded.get("dividends") is not None + else None + ), + incentives=( + [u16tf(ic) for ic in decoded.get("incentives", [])] + if decoded.get("incentives") is not None + else None + ), + consensus=( + [u16tf(cs) for cs in decoded.get("consensus", [])] + if decoded.get("consensus") is not None + else None + ), + trust=( + [u16tf(tr) for tr in decoded.get("trust", [])] + if decoded.get("trust") is not None + else None + ), + rank=( + [u16tf(rk) for rk in decoded.get("rank", [])] + if decoded.get("rank") is not None + else None + ), + block_at_registration=decoded["block_at_registration"], + alpha_stake=( + [_tbwu(ast, _netuid) for ast in decoded["alpha_stake"]] + if decoded.get("alpha_stake") is not None + else None + ), + tao_stake=( + [ + _tbwu(ts) * settings.ROOT_TAO_STAKE_WEIGHT + for ts in decoded["tao_stake"] + ] + if decoded.get("tao_stake") is not None + else None + ), + total_stake=( + [_tbwu(ts, _netuid) for ts in decoded["total_stake"]] + if decoded.get("total_stake") is not None + else None + ), + # 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 + 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 + else None + ), + validators=[v for v in decoded["validators"]] + if decoded.get("validators") is not None + else None, + ) + + +@dataclass +class MetagraphInfoEmissions: + """Emissions presented in tao values.""" + + subnet_emission: float + alpha_in_emission: float + alpha_out_emission: float + tao_in_emission: float + pending_alpha_emission: float + pending_root_emission: float + + +@dataclass +class MetagraphInfoPool: + """Pool presented in tao values.""" + + alpha_out: float + alpha_in: float + tao_in: float + subnet_volume: float + moving_price: float + + +@dataclass +class MetagraphInfoParams: + activity_cutoff: int + adjustment_alpha: float + adjustment_interval: int + alpha_high: float + alpha_low: float + bonds_moving_avg: float + burn: float + commit_reveal_period: int + commit_reveal_weights_enabled: bool + difficulty: float + immunity_period: int + kappa: float + liquid_alpha_enabled: bool + max_burn: float + max_difficulty: float + max_regs_per_block: int + max_validators: int + max_weights_limit: float + min_allowed_weights: float + min_burn: float + min_difficulty: float + pow_registration_allowed: bool + registration_allowed: bool + rho: int + serving_rate_limit: int + target_regs_per_interval: int + tempo: int + weights_rate_limit: int + weights_version: int + + +class SelectiveMetagraphIndex(Enum): + Netuid = 0 + Name = 1 + Symbol = 2 + Identity = 3 + NetworkRegisteredAt = 4 + OwnerHotkey = 5 + OwnerColdkey = 6 + Block = 7 + Tempo = 8 + LastStep = 9 + BlocksSinceLastStep = 10 + SubnetEmission = 11 + AlphaIn = 12 + AlphaOut = 13 + TaoIn = 14 + AlphaOutEmission = 15 + AlphaInEmission = 16 + TaoInEmission = 17 + PendingAlphaEmission = 18 + PendingRootEmission = 19 + SubnetVolume = 20 + MovingPrice = 21 + Rho = 22 + Kappa = 23 + MinAllowedWeights = 24 + MaxWeightsLimit = 25 + WeightsVersion = 26 + WeightsRateLimit = 27 + ActivityCutoff = 28 + MaxValidators = 29 + NumUids = 30 + MaxUids = 31 + Burn = 32 + Difficulty = 33 + RegistrationAllowed = 34 + PowRegistrationAllowed = 35 + ImmunityPeriod = 36 + MinDifficulty = 37 + MaxDifficulty = 38 + MinBurn = 39 + MaxBurn = 40 + AdjustmentAlpha = 41 + AdjustmentInterval = 42 + TargetRegsPerInterval = 43 + MaxRegsPerBlock = 44 + ServingRateLimit = 45 + CommitRevealWeightsEnabled = 46 + CommitRevealPeriod = 47 + LiquidAlphaEnabled = 48 + AlphaHigh = 49 + AlphaLow = 50 + BondsMovingAvg = 51 + Hotkeys = 52 + Coldkeys = 53 + Identities = 54 + Axons = 55 + Active = 56 + ValidatorPermit = 57 + PruningScore = 58 + LastUpdate = 59 + Emission = 60 + Dividends = 61 + Incentives = 62 + Consensus = 63 + Trust = 64 + Rank = 65 + BlockAtRegistration = 66 + AlphaStake = 67 + TaoStake = 68 + TotalStake = 69 + TaoDividendsPerHotkey = 70 + AlphaDividendsPerHotkey = 71 + Validators = 72 + + @staticmethod + def all_indices() -> list[int]: + return [member.value for member in SelectiveMetagraphIndex] diff --git a/bittensor/core/chain_data/neuron_info.py b/bittensor/core/chain_data/neuron_info.py new file mode 100644 index 0000000000..0cf917a5fb --- /dev/null +++ b/bittensor/core/chain_data/neuron_info.py @@ -0,0 +1,164 @@ +from dataclasses import dataclass +from typing import TYPE_CHECKING, Any, Optional + +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.utils import u16_normalized_float +from bittensor.utils.balance import Balance + +# for annotation purposes +if TYPE_CHECKING: + from bittensor.core.chain_data.neuron_info_lite import NeuronInfoLite + + +@dataclass +class NeuronInfo(InfoBase): + """Represents the metadata of a neuron including keys, UID, stake, rankings, and other attributes. + + Attributes: + hotkey (str): The hotkey associated with the neuron. + coldkey (str): The coldkey associated with the neuron. + uid (int): The unique identifier for the neuron. + netuid (int): The network unique identifier for the neuron. + active (int): The active status of the neuron. + stake (Balance): The balance staked to this neuron. + stake_dict (dict[str, Balance]): A dictionary mapping coldkey to the amount staked. + total_stake (Balance): The total amount of stake. + rank (float): The rank score of the neuron. + emission (float): The emission rate. + incentive (float): The incentive value. + consensus (float): The consensus score. + trust (float): The trust score. + validator_trust (float): The validation trust score. + dividends (float): The dividends value. + last_update (int): The timestamp of the last update. + validator_permit (bool): Validator permit status. + weights (list[tuple[int]]): List of weights associated with the neuron. + bonds (list[list[int]]): List of bonds associated with the neuron. + pruning_score (int): The pruning score of the neuron. + prometheus_info (Optional[PrometheusInfo]): Information related to Prometheus. + axon_info (Optional[AxonInfo]): Information related to Axon. + is_null (bool): Indicator if this is a null neuron. + """ + + hotkey: str + coldkey: str + uid: int + netuid: int + active: int + stake: "Balance" + # mapping of coldkey to amount staked to this Neuron + stake_dict: dict[str, "Balance"] + total_stake: "Balance" + rank: float + emission: float + incentive: float + consensus: float + trust: float + validator_trust: float + dividends: float + last_update: int + validator_permit: bool + weights: list[tuple[int, int]] + bonds: list[list[int]] + pruning_score: int + prometheus_info: Optional["PrometheusInfo"] = None + axon_info: Optional["AxonInfo"] = None + is_null: bool = False + + @classmethod + def from_weights_bonds_and_neuron_lite( + cls, + neuron_lite: "NeuronInfoLite", + weights_as_dict: dict[int, list[tuple[int, int]]], + bonds_as_dict: dict[int, list[tuple[int, int]]], + ) -> "NeuronInfo": + """ + Creates an instance of NeuronInfo from NeuronInfoLite and dictionaries of weights and bonds. + + Args: + neuron_lite (NeuronInfoLite): A lite version of the neuron containing basic attributes. + weights_as_dict (dict[int, list[tuple[int, int]]]): A dictionary where the key is the UID and the value is + a list of weight tuples associated with the neuron. + bonds_as_dict (dict[int, list[tuple[int, int]]]): A dictionary where the key is the UID and the value is a + list of bond tuples associated with the neuron. + + Returns: + NeuronInfo: An instance of NeuronInfo populated with the provided weights and bonds. + """ + n_dict = neuron_lite.__dict__ + n_dict["weights"] = weights_as_dict.get(neuron_lite.uid, []) + n_dict["bonds"] = bonds_as_dict.get(neuron_lite.uid, []) + + return cls(**n_dict) + + @staticmethod + def get_null_neuron() -> "NeuronInfo": + """Returns a null NeuronInfo instance.""" + neuron = NeuronInfo( + uid=0, + netuid=0, + active=0, + stake=Balance.from_rao(0), + stake_dict={}, + total_stake=Balance.from_rao(0), + rank=0, + emission=0, + incentive=0, + consensus=0, + trust=0, + validator_trust=0, + dividends=0, + last_update=0, + validator_permit=False, + weights=[], + bonds=[], + prometheus_info=None, + axon_info=None, + is_null=True, + coldkey="000000000000000000000000000000000000000000000000", + hotkey="000000000000000000000000000000000000000000000000", + pruning_score=0, + ) + return neuron + + @classmethod + 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"]) + return NeuronInfo( + active=decoded["active"], + axon_info=AxonInfo.from_dict( + decoded["axon_info"] + | { + "hotkey": hotkey, + "coldkey": coldkey, + }, + ), + bonds=[[e[0], e[1]] for e in decoded["bonds"]], + coldkey=coldkey, + consensus=u16_normalized_float(decoded["consensus"]), + dividends=u16_normalized_float(decoded["dividends"]), + emission=decoded["emission"] / 1e9, + hotkey=hotkey, + incentive=u16_normalized_float(decoded["incentive"]), + is_null=False, + last_update=decoded["last_update"], + netuid=decoded["netuid"], + prometheus_info=PrometheusInfo.from_dict(decoded["prometheus_info"]), + pruning_score=decoded["pruning_score"], + rank=u16_normalized_float(decoded["rank"]), + stake_dict=stake_dict, + stake=total_stake, + total_stake=total_stake, + trust=u16_normalized_float(decoded["trust"]), + uid=decoded["uid"], + validator_permit=decoded["validator_permit"], + validator_trust=u16_normalized_float(decoded["validator_trust"]), + weights=[(e[0], e[1]) for e in decoded["weights"]], + ) diff --git a/bittensor/core/chain_data/neuron_info_lite.py b/bittensor/core/chain_data/neuron_info_lite.py new file mode 100644 index 0000000000..5dd60cef82 --- /dev/null +++ b/bittensor/core/chain_data/neuron_info_lite.py @@ -0,0 +1,131 @@ +from dataclasses import dataclass +from typing import Any, Optional + +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.utils import u16_normalized_float +from bittensor.utils.balance import Balance + + +@dataclass +class NeuronInfoLite(InfoBase): + """ + NeuronInfoLite is a dataclass representing neuron metadata without weights and bonds. + + Attributes: + hotkey (str): The hotkey string for the neuron. + coldkey (str): The coldkey string for the neuron. + uid (int): A unique identifier for the neuron. + netuid (int): Network unique identifier for the neuron. + active (int): Indicates whether the neuron is active. + stake (Balance): The stake amount associated with the neuron. + stake_dict (dict): Mapping of coldkey to the amount staked to this Neuron. + total_stake (Balance): Total amount of the stake. + rank (float): The rank of the neuron. + emission (float): The emission value of the neuron. + incentive (float): The incentive value of the neuron. + consensus (float): The consensus value of the neuron. + trust (float): Trust value of the neuron. + validator_trust (float): Validator trust value of the neuron. + dividends (float): Dividends associated with the neuron. + last_update (int): Timestamp of the last update. + validator_permit (bool): Indicates if the neuron has a validator permit. + prometheus_info (Optional[PrometheusInfo]): Prometheus information associated with the neuron. + axon_info (Optional[AxonInfo]): Axon information associated with the neuron. + pruning_score (int): The pruning score of the neuron. + is_null (bool): Indicates whether the neuron is null. + + Methods: + get_null_neuron: Returns a NeuronInfoLite object representing a null neuron. + list_from_vec_u8: Decodes a bytes object into a list of NeuronInfoLite instances. + """ + + hotkey: str + coldkey: str + uid: int + netuid: int + active: int + stake: "Balance" + # mapping of coldkey to amount staked to this Neuron + stake_dict: dict[str, "Balance"] + total_stake: "Balance" + rank: float + emission: float + incentive: float + consensus: float + trust: float + validator_trust: float + dividends: float + last_update: int + validator_permit: bool + prometheus_info: Optional["PrometheusInfo"] + axon_info: Optional["AxonInfo"] + pruning_score: int + is_null: bool = False + + @staticmethod + def get_null_neuron() -> "NeuronInfoLite": + """Returns a null NeuronInfoLite instance.""" + neuron = NeuronInfoLite( + uid=0, + netuid=0, + active=0, + stake=Balance.from_rao(0), + stake_dict={}, + total_stake=Balance.from_rao(0), + rank=0, + emission=0, + incentive=0, + consensus=0, + trust=0, + validator_trust=0, + dividends=0, + last_update=0, + validator_permit=False, + prometheus_info=None, + axon_info=None, + is_null=True, + coldkey="000000000000000000000000000000000000000000000000", + hotkey="000000000000000000000000000000000000000000000000", + pruning_score=0, + ) + return neuron + + @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"]) + stake_dict = process_stake_data(decoded["stake"]) + stake = sum(stake_dict.values()) if stake_dict else Balance(0) + + return NeuronInfoLite( + active=decoded["active"], + axon_info=AxonInfo.from_dict( + decoded["axon_info"] + | { + "hotkey": hotkey, + "coldkey": coldkey, + }, + ), + coldkey=coldkey, + consensus=u16_normalized_float(decoded["consensus"]), + dividends=u16_normalized_float(decoded["dividends"]), + emission=decoded["emission"] / 1e9, + hotkey=hotkey, + incentive=u16_normalized_float(decoded["incentive"]), + last_update=decoded["last_update"], + netuid=decoded["netuid"], + prometheus_info=PrometheusInfo.from_dict(decoded["prometheus_info"]), + pruning_score=decoded["pruning_score"], + rank=u16_normalized_float(decoded["rank"]), + stake_dict=stake_dict, + stake=stake, + total_stake=stake, + trust=u16_normalized_float(decoded["trust"]), + uid=decoded["uid"], + validator_permit=decoded["validator_permit"], + validator_trust=u16_normalized_float(decoded["validator_trust"]), + ) diff --git a/bittensor/core/chain_data/prometheus_info.py b/bittensor/core/chain_data/prometheus_info.py new file mode 100644 index 0000000000..6aac398d72 --- /dev/null +++ b/bittensor/core/chain_data/prometheus_info.py @@ -0,0 +1,36 @@ +from dataclasses import dataclass + +import netaddr + +from bittensor.core.chain_data.info_base import InfoBase + + +@dataclass +class PrometheusInfo(InfoBase): + """ + Dataclass representing information related to Prometheus. + + Attributes: + block (int): The block number associated with the Prometheus data. + version (int): The version of the Prometheus data. + ip (str): The IP address associated with Prometheus. + port (int): The port number for Prometheus. + ip_type (int): The type of IP address (e.g., IPv4, IPv6). + """ + + block: int + version: int + ip: str + port: int + ip_type: int + + @classmethod + def _from_dict(cls, data): + """Returns a PrometheusInfo object from decoded chain data.""" + return cls( + block=data["block"], + ip_type=data["ip_type"], + ip=str(netaddr.IPAddress(data["ip"])), + port=data["port"], + version=data["version"], + ) diff --git a/bittensor/core/chain_data/proposal_vote_data.py b/bittensor/core/chain_data/proposal_vote_data.py new file mode 100644 index 0000000000..3cf5439955 --- /dev/null +++ b/bittensor/core/chain_data/proposal_vote_data.py @@ -0,0 +1,27 @@ +from dataclasses import dataclass + +from bittensor.core.chain_data.info_base import InfoBase +from bittensor.core.chain_data.utils import decode_account_id + + +@dataclass +class ProposalVoteData(InfoBase): + """ + Senate / Proposal data + """ + + index: int + threshold: int + ayes: list[str] + nays: list[str] + end: int + + @classmethod + def from_dict(cls, proposal_dict: dict) -> "ProposalVoteData": + return cls( + ayes=[decode_account_id(key) for key in proposal_dict["ayes"]], + end=proposal_dict["end"], + index=proposal_dict["index"], + nays=[decode_account_id(key) for key in proposal_dict["nays"]], + threshold=proposal_dict["threshold"], + ) diff --git a/bittensor/core/chain_data/scheduled_coldkey_swap_info.py b/bittensor/core/chain_data/scheduled_coldkey_swap_info.py new file mode 100644 index 0000000000..d5717a25fc --- /dev/null +++ b/bittensor/core/chain_data/scheduled_coldkey_swap_info.py @@ -0,0 +1,43 @@ +from dataclasses import dataclass +from typing import Optional + +from scalecodec.utils.ss58 import ss58_encode + +from bittensor.core.chain_data.info_base import InfoBase +from bittensor.core.chain_data.utils import from_scale_encoding, ChainDataType +from bittensor.core.settings import SS58_FORMAT + + +@dataclass +class ScheduledColdkeySwapInfo(InfoBase): + """ + The `ScheduledColdkeySwapInfo` class is a dataclass representing information about scheduled cold key swaps. + + Attributes: + old_coldkey (str): The old cold key before the swap. + new_coldkey (str): The new cold key after the swap. + arbitration_block (int): The block number at which the arbitration of the swap will take place. + """ + + old_coldkey: str + new_coldkey: str + arbitration_block: int + + @classmethod + def _from_dict(cls, decoded: dict) -> "ScheduledColdkeySwapInfo": + """Returns a ScheduledColdkeySwapInfo object from decoded chain data.""" + return cls( + arbitration_block=decoded["arbitration_block"], + new_coldkey=ss58_encode(decoded["new_coldkey"], SS58_FORMAT), + old_coldkey=ss58_encode(decoded["old_coldkey"], SS58_FORMAT), + ) + + @classmethod + def decode_account_id_list(cls, vec_u8: list[int]) -> Optional[list[str]]: + """Decodes a list of AccountIds from vec_u8.""" + decoded = from_scale_encoding( + vec_u8, ChainDataType.ScheduledColdkeySwapInfo.AccountId, is_vec=True + ) + if decoded is None: + return None + return [ss58_encode(account_id, SS58_FORMAT) for account_id in decoded] diff --git a/bittensor/core/chain_data/stake_info.py b/bittensor/core/chain_data/stake_info.py new file mode 100644 index 0000000000..ade4515743 --- /dev/null +++ b/bittensor/core/chain_data/stake_info.py @@ -0,0 +1,41 @@ +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 + + +@dataclass +class StakeInfo(InfoBase): + """ + Dataclass for representing stake information linked to hotkey and coldkey pairs. + + Attributes: + hotkey_ss58 (str): The SS58 encoded hotkey address. + coldkey_ss58 (str): The SS58 encoded coldkey address. + stake (Balance): The stake associated with the hotkey-coldkey pair, represented as a Balance object. + """ + + hotkey_ss58: str # Hotkey address + coldkey_ss58: str # Coldkey address + netuid: int # Network UID + stake: Balance # Stake for the hotkey-coldkey pair + locked: Balance # Stake which is locked. + emission: Balance # Emission for the hotkey-coldkey pair + drain: int + is_registered: bool + + @classmethod + 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"]), + netuid=int(netuid), + stake=Balance.from_rao(decoded["stake"]).set_unit(netuid), + locked=Balance.from_rao(decoded["locked"]).set_unit(netuid), + emission=Balance.from_rao(decoded["emission"]).set_unit(netuid), + drain=int(decoded["drain"]), + is_registered=bool(decoded["is_registered"]), + ) diff --git a/bittensor/core/chain_data/subnet_hyperparameters.py b/bittensor/core/chain_data/subnet_hyperparameters.py new file mode 100644 index 0000000000..206e44e7d4 --- /dev/null +++ b/bittensor/core/chain_data/subnet_hyperparameters.py @@ -0,0 +1,120 @@ +from dataclasses import dataclass +from bittensor.utils.balance import fixed_to_float +from bittensor.core.chain_data.info_base import InfoBase + + +@dataclass +class SubnetHyperparameters(InfoBase): + """ + This class represents the hyperparameters for a subnet. + + Attributes: + rho (int): The rate of decay of some value. + kappa (int): A constant multiplier used in calculations. + immunity_period (int): The period during which immunity is active. + min_allowed_weights (int): Minimum allowed weights. + max_weight_limit (float): Maximum weight limit. + tempo (int): The tempo or rate of operation. + min_difficulty (int): Minimum difficulty for some operations. + max_difficulty (int): Maximum difficulty for some operations. + weights_version (int): The version number of the weights used. + weights_rate_limit (int): Rate limit for processing weights. + adjustment_interval (int): Interval at which adjustments are made. + activity_cutoff (int): Activity cutoff threshold. + registration_allowed (bool): Indicates if registration is allowed. + target_regs_per_interval (int): Target number of registrations per interval. + min_burn (int): Minimum burn value. + max_burn (int): Maximum burn value. + bonds_moving_avg (int): Moving average of bonds. + max_regs_per_block (int): Maximum number of registrations per block. + serving_rate_limit (int): Limit on the rate of service. + max_validators (int): Maximum number of validators. + adjustment_alpha (int): Alpha value for adjustments. + difficulty (int): Difficulty level. + commit_reveal_period (int): Interval for commit-reveal weights. + commit_reveal_weights_enabled (bool): Flag indicating if commit-reveal weights are enabled. + alpha_high (int): High value of alpha. + alpha_low (int): Low value of alpha. + liquid_alpha_enabled (bool): Flag indicating if liquid alpha is enabled. + alpha_sigmoid_steepness (float): + yuma_version (int): Version of yuma. + subnet_is_active (bool): Indicates if subnet is active after START CALL. + transfers_enabled (bool): Flag indicating if transfers are enabled. + bonds_reset_enabled (bool): Flag indicating if bonds are reset enabled. + user_liquidity_enabled (bool): Flag indicating if user liquidity is enabled. + """ + + rho: int + kappa: int + immunity_period: int + min_allowed_weights: int + max_weight_limit: float + tempo: int + min_difficulty: int + max_difficulty: int + weights_version: int + weights_rate_limit: int + adjustment_interval: int + activity_cutoff: int + registration_allowed: bool + target_regs_per_interval: int + min_burn: int + max_burn: int + bonds_moving_avg: int + max_regs_per_block: int + serving_rate_limit: int + max_validators: int + adjustment_alpha: int + difficulty: int + commit_reveal_period: int + commit_reveal_weights_enabled: bool + alpha_high: int + alpha_low: int + liquid_alpha_enabled: bool + alpha_sigmoid_steepness: float + yuma_version: int + subnet_is_active: bool + transfers_enabled: bool + bonds_reset_enabled: bool + user_liquidity_enabled: bool + + @classmethod + def _from_dict(cls, decoded: dict) -> "SubnetHyperparameters": + """Returns a SubnetHyperparameters object from decoded chain data.""" + return SubnetHyperparameters( + activity_cutoff=decoded["activity_cutoff"], + adjustment_alpha=decoded["adjustment_alpha"], + adjustment_interval=decoded["adjustment_interval"], + alpha_high=decoded["alpha_high"], + alpha_low=decoded["alpha_low"], + alpha_sigmoid_steepness=fixed_to_float( + decoded["alpha_sigmoid_steepness"], frac_bits=32 + ), + bonds_moving_avg=decoded["bonds_moving_avg"], + bonds_reset_enabled=decoded["bonds_reset_enabled"], + commit_reveal_weights_enabled=decoded["commit_reveal_weights_enabled"], + commit_reveal_period=decoded["commit_reveal_period"], + difficulty=decoded["difficulty"], + immunity_period=decoded["immunity_period"], + kappa=decoded["kappa"], + liquid_alpha_enabled=decoded["liquid_alpha_enabled"], + max_burn=decoded["max_burn"], + max_difficulty=decoded["max_difficulty"], + max_regs_per_block=decoded["max_regs_per_block"], + max_validators=decoded["max_validators"], + max_weight_limit=decoded["max_weights_limit"], + min_allowed_weights=decoded["min_allowed_weights"], + min_burn=decoded["min_burn"], + min_difficulty=decoded["min_difficulty"], + registration_allowed=decoded["registration_allowed"], + rho=decoded["rho"], + serving_rate_limit=decoded["serving_rate_limit"], + subnet_is_active=decoded["subnet_is_active"], + target_regs_per_interval=decoded["target_regs_per_interval"], + tempo=decoded["tempo"], + transfers_enabled=decoded["transfers_enabled"], + user_liquidity_enabled=decoded["user_liquidity_enabled"], + weights_rate_limit=decoded["weights_rate_limit"], + weights_version=decoded["weights_version"], + yuma_version=decoded["yuma_version"], + ) diff --git a/bittensor/core/chain_data/subnet_identity.py b/bittensor/core/chain_data/subnet_identity.py new file mode 100644 index 0000000000..83e0866ea4 --- /dev/null +++ b/bittensor/core/chain_data/subnet_identity.py @@ -0,0 +1,29 @@ +from dataclasses import dataclass + + +@dataclass +class SubnetIdentity: + """Dataclass for subnet identity information.""" + + subnet_name: str + github_repo: str + subnet_contact: str + subnet_url: str + logo_url: str + discord: str + description: str + additional: str + + @classmethod + def _from_dict(cls, decoded: dict) -> "SubnetIdentity": + """Returns a SubnetIdentity object from decoded chain data.""" + return cls( + subnet_name=decoded["subnet_name"], + github_repo=decoded["github_repo"], + subnet_contact=decoded["subnet_contact"], + subnet_url=decoded["subnet_url"], + logo_url=decoded["logo_url"], + discord=decoded["discord"], + description=decoded["description"], + additional=decoded["additional"], + ) diff --git a/bittensor/core/chain_data/subnet_info.py b/bittensor/core/chain_data/subnet_info.py new file mode 100644 index 0000000000..978dab29f7 --- /dev/null +++ b/bittensor/core/chain_data/subnet_info.py @@ -0,0 +1,58 @@ +from dataclasses import dataclass +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 + + +@dataclass +class SubnetInfo(InfoBase): + """Dataclass for subnet info.""" + + netuid: int + rho: int + kappa: int + difficulty: int + immunity_period: int + max_allowed_validators: int + min_allowed_weights: int + max_weight_limit: float + scaling_law_power: float + subnetwork_n: int + max_n: int + blocks_since_epoch: int + tempo: int + modality: int + connection_requirements: dict[str, float] + emission_value: float + burn: Balance + owner_ss58: str + + @classmethod + def _from_dict(cls, decoded: Any) -> "SubnetInfo": + """Returns a SubnetInfo object from decoded chain data.""" + return SubnetInfo( + blocks_since_epoch=decoded["blocks_since_last_step"], + burn=Balance.from_rao(decoded["burn"]), + connection_requirements={ + str(int(netuid)): u16_normalized_float(int(req)) + for (netuid, req) in decoded["network_connect"] + }, + difficulty=decoded["difficulty"], + emission_value=decoded["emission_value"], + immunity_period=decoded["immunity_period"], + kappa=decoded["kappa"], + max_allowed_validators=decoded["max_allowed_validators"], + max_n=decoded["max_allowed_uids"], + max_weight_limit=decoded["max_weights_limit"], + min_allowed_weights=decoded["min_allowed_weights"], + modality=decoded["network_modality"], + netuid=decoded["netuid"], + owner_ss58=decode_account_id(decoded["owner"]), + rho=decoded["rho"], + scaling_law_power=decoded["scaling_law_power"], + subnetwork_n=decoded["subnetwork_n"], + tempo=decoded["tempo"], + ) diff --git a/bittensor/core/chain_data/subnet_state.py b/bittensor/core/chain_data/subnet_state.py new file mode 100644 index 0000000000..caf01bfc2c --- /dev/null +++ b/bittensor/core/chain_data/subnet_state.py @@ -0,0 +1,68 @@ +""" +This module defines the `SubnetState` data class and associated methods for handling and decoding +subnetwork states in the Bittensor network. +""" + +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 + + +@dataclass +class SubnetState(InfoBase): + netuid: int + hotkeys: list[str] + coldkeys: list[str] + active: list[bool] + validator_permit: list[bool] + pruning_score: list[float] + last_update: list[int] + emission: list["Balance"] + dividends: list[float] + incentives: list[float] + consensus: list[float] + trust: list[float] + rank: list[float] + block_at_registration: list[int] + alpha_stake: list["Balance"] + tao_stake: list["Balance"] + total_stake: list["Balance"] + emission_history: list[list[int]] + + @classmethod + def _from_dict(cls, decoded: dict) -> "SubnetState": + """Returns a SubnetState object from decoded chain data.""" + 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", [])], + active=decoded["active"], + validator_permit=decoded["validator_permit"], + pruning_score=[ + u16_normalized_float(val) for val in decoded["pruning_score"] + ], + last_update=decoded["last_update"], + emission=[ + Balance.from_rao(val).set_unit(netuid) for val in decoded["emission"] + ], + dividends=[u16_normalized_float(val) for val in decoded["dividends"]], + incentives=[u16_normalized_float(val) for val in decoded["incentives"]], + consensus=[u16_normalized_float(val) for val in decoded["consensus"]], + trust=[u16_normalized_float(val) for val in decoded["trust"]], + rank=[u16_normalized_float(val) for val in decoded["rank"]], + block_at_registration=decoded["block_at_registration"], + alpha_stake=[ + Balance.from_rao(val).set_unit(netuid) for val in decoded["alpha_stake"] + ], + tao_stake=[ + Balance.from_rao(val).set_unit(0) for val in decoded["tao_stake"] + ], + total_stake=[ + Balance.from_rao(val).set_unit(netuid) for val in decoded["total_stake"] + ], + emission_history=decoded["emission_history"], + ) diff --git a/bittensor/core/chain_data/utils.py b/bittensor/core/chain_data/utils.py new file mode 100644 index 0000000000..b1a81ce517 --- /dev/null +++ b/bittensor/core/chain_data/utils.py @@ -0,0 +1,201 @@ +"""Chain data helper functions and data.""" + +from enum import Enum +from typing import Optional, Union, TYPE_CHECKING + +from scalecodec.base import RuntimeConfiguration, ScaleBytes +from async_substrate_interface.types import ScaleObj +from scalecodec.type_registry import load_type_registry_preset +from scalecodec.utils.ss58 import ss58_encode + +from bittensor.core.settings import SS58_FORMAT +from bittensor.utils.balance import Balance + +if TYPE_CHECKING: + from async_substrate_interface.sync_substrate import QueryMapResult + + +class ChainDataType(Enum): + NeuronInfo = 1 + SubnetInfo = 2 + DelegateInfo = 3 + NeuronInfoLite = 4 + DelegatedInfo = 5 + StakeInfo = 6 + IPInfo = 7 + SubnetHyperparameters = 8 + ScheduledColdkeySwapInfo = 9 + AccountId = 10 + SubnetState = 11 + DynamicInfo = 12 + SubnetIdentity = 13 + MetagraphInfo = 14 + ChainIdentity = 15 + AxonInfo = 16 + + +def from_scale_encoding( + input_: Union[list[int], bytes, "ScaleBytes"], + type_name: "ChainDataType", + is_vec: bool = False, + is_option: bool = False, +) -> Optional[dict]: + """ + Decodes input_ data from SCALE encoding based on the specified type name and modifiers. + + Args: + input_ (Union[List[int], bytes, ScaleBytes]): The input_ data to decode. + type_name (ChainDataType): The type of data being decoded. + is_vec (bool): Whether the data is a vector of the specified type. Default is ``False``. + is_option (bool): Whether the data is an optional value of the specified type. Default is ``False``. + + Returns: + Optional[dict]: The decoded data as a dictionary, or ``None`` if the decoding fails. + """ + type_string = type_name.name + if type_name == ChainDataType.DelegatedInfo: + # DelegatedInfo is a tuple of (DelegateInfo, Compact) + type_string = f"({ChainDataType.DelegateInfo.name}, Compact)" + if is_option: + type_string = f"Option<{type_string}>" + if is_vec: + type_string = f"Vec<{type_string}>" + + return from_scale_encoding_using_type_string(input_, type_string) + + +def from_scale_encoding_using_type_string( + input_: Union[list[int], bytes, ScaleBytes], type_string: str +) -> Optional[dict]: + """ + Decodes SCALE encoded data to a dictionary based on the provided type string. + + Args: + input_ (Union[List[int], bytes, ScaleBytes]): The SCALE encoded input data. + type_string (str): The type string defining the structure of the data. + + Returns: + Optional[dict]: The decoded data as a dictionary, or ``None`` if the decoding fails. + + Raises: + TypeError: If the input_ is not a list[int], bytes, or ScaleBytes. + """ + if isinstance(input_, ScaleBytes): + as_scale_bytes = input_ + else: + if isinstance(input_, list) and all([isinstance(i, int) for i in input_]): + vec_u8 = input_ + as_bytes = bytes(vec_u8) + elif isinstance(input_, bytes): + as_bytes = input_ + else: + raise TypeError("input_ must be a list[int], bytes, or ScaleBytes") + + as_scale_bytes = ScaleBytes(as_bytes) + + rpc_runtime_config = RuntimeConfiguration() + rpc_runtime_config.update_type_registry(load_type_registry_preset("legacy")) + + obj = rpc_runtime_config.create_scale_object(type_string, data=as_scale_bytes) + + 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. + + Args: + account_id_bytes (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. + + Args: + stake_data (list): A list of tuples where each tuple contains an account ID in bytes and a stake in rao. + + Returns: + 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) + 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] + bytes_tuple_ = commitment[next(iter(commitment.keys()))] + bytes_tuple = bytes_tuple_[0] if len(bytes_tuple_) > 0 else bytes_tuple_ + return bytes(bytes_tuple).decode() + + +def decode_block(data: bytes) -> int: + """ + Decode the block data from the given input if it is not None. + + Arguments: + data (bytes): The block data to decode. + + Returns: + int: The decoded block. + """ + return int(data.value) if isinstance(data, ScaleObj) else data + + +def decode_revealed_commitment(encoded_data) -> tuple[int, str]: + """ + Decode the revealed commitment data from the given input if it is not None. + + Arguments: + encoded_data (tuple[bytes, int]): A tuple containing the revealed message and the block number. + + Returns: + tuple[int, str]: A tuple containing the revealed block number and decoded commitment message. + """ + + def scale_decode_offset(data: bytes) -> int: + """Decodes the scale offset from a given byte data sequence.""" + first_byte = data[0] + mode = first_byte & 0b11 + if mode == 0: + return 1 + elif mode == 1: + return 2 + else: + return 4 + + com_bytes, revealed_block = encoded_data + offset = scale_decode_offset(com_bytes) + + revealed_commitment = bytes(com_bytes[offset:]).decode("utf-8", errors="ignore") + return revealed_block, revealed_commitment + + +def decode_revealed_commitment_with_hotkey( + encoded_data: "QueryMapResult", +) -> tuple[str, tuple[tuple[int, str], ...]]: + """ + Decode revealed commitment using a hotkey. + + Returns: + tuple[str, tuple[tuple[int, str], ...]]: A tuple containing the hotkey (ss58 address) and a tuple of block + numbers and their corresponding revealed commitments. + """ + key, data = encoded_data + + ss58_address = decode_account_id(next(iter(key))) + block_data = tuple(decode_revealed_commitment(p) for p in data.value) + return ss58_address, block_data diff --git a/bittensor/core/chain_data/weight_commit_info.py b/bittensor/core/chain_data/weight_commit_info.py new file mode 100644 index 0000000000..b8555eed68 --- /dev/null +++ b/bittensor/core/chain_data/weight_commit_info.py @@ -0,0 +1,66 @@ +from dataclasses import dataclass +from bittensor.core.chain_data.utils import decode_account_id +from typing import Optional + + +@dataclass +class WeightCommitInfo: + """ + Data class representing weight commit information. + + Attributes: + ss58: The SS58 address of the committer + commit_block: The block number of the commitment. + commit_hex: The serialized weight commit data as hex string + reveal_round: The round number for reveal + """ + + ss58: str + commit_block: Optional[int] + commit_hex: str + reveal_round: int + + @classmethod + def from_vec_u8(cls, data: tuple) -> tuple[str, str, int]: + """ + Creates a WeightCommitInfo instance + + Args: + data (tuple): Tuple containing ((AccountId,), (commit_data,), round_number) + + Returns: + WeightCommitInfo: A new instance with the decoded data + + Note: + 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_ = 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 + + @classmethod + def from_vec_u8_v2(cls, data: tuple) -> tuple[str, int, str, int]: + """ + Creates a WeightCommitInfo instance + + Args: + data (tuple): Tuple containing ((AccountId,), (commit_block, ) (commit_data,), round_number) + + 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) + + return decode_account_id(account_id_), commit_block, commit_hex, round_number diff --git a/bittensor/core/config.py b/bittensor/core/config.py new file mode 100644 index 0000000000..2fb3d8495b --- /dev/null +++ b/bittensor/core/config.py @@ -0,0 +1,248 @@ +"""Implementation of the config class, which manages the configuration of different Bittensor modules. + +Example: + import argparse + import bittensor as bt + + parser = argparse.ArgumentParser('Miner') + bt.Axon.add_args(parser) + bt.Subtensor.add_args(parser) + bt.Async_subtensor.add_args(parser) + bt.Wallet.add_args(parser) + bt.logging.add_args(parser) + bt.PriorityThreadPoolExecutor.add_args(parser) + config = bt.config(parser) + + print(config) +""" + +import argparse +import os +import sys +from copy import deepcopy +from typing import Any, TypeVar, Type, Optional + +import yaml +from munch import DefaultMunch + + +def _filter_keys(obj): + """Filters keys from an object, excluding private and certain internal properties.""" + if isinstance(obj, dict): + return { + k: _filter_keys(v) + for k, v in obj.items() + if not k.startswith("__") and not k.startswith("_Config__is_set") + } + elif isinstance(obj, (Config, DefaultMunch)): + return _filter_keys(obj.toDict()) + return obj + + +class InvalidConfigFile(Exception): + """Raised when there's an error loading the config file.""" + + +class Config(DefaultMunch): + """Manages configuration for Bittensor modules with nested namespace support.""" + + def __init__( + self, + parser: argparse.ArgumentParser = None, + args: Optional[list[str]] = None, + strict: bool = False, + default: Any = None, + ) -> None: + super().__init__(default) + self.__is_set = {} + + if parser is None: + return + + self._add_default_arguments(parser) + args = args or sys.argv[1:] + self._validate_required_args(parser, args) + + config_params = self._parse_args(args, parser, strict=False) + config_path = self._get_config_path(config_params) + strict = strict or getattr(config_params, "strict", False) + + if config_path: + self._load_config_file(parser, config_path) + + params = self._parse_args(args, parser, strict) + self._build_config_tree(params) + self._detect_set_parameters(parser, args) + + def __str__(self) -> str: + """String representation without private keys, optimized to avoid deepcopy.""" + cleaned = _filter_keys(self.toDict()) + return "\n" + yaml.dump(cleaned, sort_keys=False, default_flow_style=False) + + def __repr__(self) -> str: + """String representation of the Config.""" + return self.__str__() + + def _validate_required_args( + self, parser: argparse.ArgumentParser, args: list[str] + ) -> None: + """Validates required arguments are present.""" + missing = self._find_missing_required_args(parser, args) + if missing: + raise ValueError(f"Missing required arguments: {', '.join(missing)}") + + def _find_missing_required_args( + self, parser: argparse.ArgumentParser, args: list[str] + ) -> list[str]: + """Identifies missing required arguments.""" + required = {a.dest for a in parser._actions if a.required} + provided = {a.split("=")[0].lstrip("-") for a in args if a.startswith("-")} + return list(required - provided) + + def _get_config_path(self, params: DefaultMunch) -> Optional[str]: + """Gets Config path from parameters.""" + return getattr(params, "config", None) + + def _load_config_file(self, parser: argparse.ArgumentParser, path: str) -> None: + """Loads Config from YAML file.""" + try: + with open(os.path.expanduser(path)) as f: + config = yaml.safe_load(f) + print(f"Loading config from: {path}") + parser.set_defaults(**config) + except Exception as e: + raise InvalidConfigFile(f"Error loading config: {e}") from e + + def _build_config_tree(self, params: DefaultMunch) -> None: + """Builds nested Config structure.""" + for key, value in params.items(): + if key in ["__is_set"]: + continue + current = self + parts = key.split(".") + for part in parts[:-1]: + current = current.setdefault(part, Config()) + current[parts[-1]] = value + + def _detect_set_parameters( + self, parser: argparse.ArgumentParser, args: list[str] + ) -> None: + """Detects which parameters were explicitly set.""" + temp_parser = self._create_non_default_parser(parser) + detected = self._parse_args(args, temp_parser, strict=False) + self.__is_set = DefaultMunch(**{k: True for k in detected.keys()}) + + def _create_non_default_parser( + self, original: argparse.ArgumentParser + ) -> argparse.ArgumentParser: + """Creates a parser that ignores default values.""" + parser = deepcopy(original) + for action in parser._actions: + action.default = argparse.SUPPRESS + return parser + + @staticmethod + def _parse_args( + args: list[str], parser: argparse.ArgumentParser, strict: bool + ) -> DefaultMunch: + """Parses args with error handling.""" + try: + if strict: + result = parser.parse_args(args) + return DefaultMunch.fromDict(vars(result)) + + result, unknown = parser.parse_known_args(args) + for arg in unknown: + if arg.startswith("--") and (name := arg[2:]) in vars(result): + setattr(result, name, True) + return DefaultMunch.fromDict(vars(result)) + except Exception: + raise ValueError("Invalid arguments provided.") + + def __deepcopy__(self, memo) -> "Config": + """Creates a deep copy that maintains Config type.""" + new_config = Config() + memo[id(self)] = new_config + + for key, value in self.items(): + new_config[key] = deepcopy(value, memo) + + new_config.__is_set = deepcopy(self.__is_set, memo) + return new_config + + def merge(self, other: "Config") -> None: + """Merges another Config into this one.""" + self.update(self._merge_dicts(self, other)) + self.__is_set.update(other.__is_set) + + @staticmethod + def _merge_dicts(a: DefaultMunch, b: DefaultMunch) -> DefaultMunch: + """Recursively merges two Config objects.""" + result = deepcopy(a) + for key, value in b.items(): + if key in result: + if isinstance(result[key], DefaultMunch) and isinstance( + value, DefaultMunch + ): + result[key] = Config._merge_dicts(result[key], value) + else: + result[key] = deepcopy(value) + else: + result[key] = deepcopy(value) + return result + + def is_set(self, param_name: str) -> bool: + """Checks if a parameter was explicitly set.""" + return self.__is_set.get(param_name, False) + + def to_dict(self) -> dict: + """Returns the configuration as a dictionary.""" + return self.toDict() + + def _add_default_arguments(self, parser: argparse.ArgumentParser) -> None: + """Adds default arguments to the Config parser.""" + arguments = [ + ( + "--config", + { + "type": str, + "help": "If set, defaults are overridden by passed file.", + "default": False, + }, + ), + ( + "--strict", + { + "action": "store_true", + "help": "If flagged, config will check that only exact arguments have been set.", + "default": False, + }, + ), + ( + "--no_version_checking", + { + "action": "store_true", + "help": "Set `true to stop cli version checking.", + "default": False, + }, + ), + ] + + for arg_name, kwargs in arguments: + try: + parser.add_argument(arg_name, **kwargs) + except argparse.ArgumentError: + # this can fail if argument has already been added. + pass + + +T = TypeVar("T", bound="DefaultConfig") + + +class DefaultConfig(Config): + """A Config with a set of default values.""" + + @classmethod + def default(cls: Type[T]) -> T: + """Get default config.""" + raise NotImplementedError("Function default is not implemented.") diff --git a/bittensor/core/dendrite.py b/bittensor/core/dendrite.py new file mode 100644 index 0000000000..5dcc509e16 --- /dev/null +++ b/bittensor/core/dendrite.py @@ -0,0 +1,906 @@ +from __future__ import annotations + +import asyncio +import time +import uuid +import warnings +from typing import Any, AsyncGenerator, Optional, Union, Type + +import aiohttp +from bittensor_wallet import Keypair, Wallet + +from bittensor.core.axon import Axon +from bittensor.core.chain_data import AxonInfo +from bittensor.core.settings import version_as_int +from bittensor.core.stream import StreamingSynapse +from bittensor.core.synapse import Synapse, TerminalInfo +from bittensor.utils import networking +from bittensor.utils.btlogging import logging +from bittensor.utils.registration import torch, use_torch + +DENDRITE_ERROR_MAPPING: dict[Type[Exception], tuple] = { + aiohttp.ClientConnectorError: ("503", "Service unavailable"), + asyncio.TimeoutError: ("408", "Request timeout"), + aiohttp.ClientResponseError: (None, "Client response error"), + aiohttp.ClientPayloadError: ("400", "Payload error"), + aiohttp.ClientError: ("500", "Client error"), + aiohttp.ServerTimeoutError: ("504", "Server timeout error"), + aiohttp.ServerDisconnectedError: ("503", "Service disconnected"), + aiohttp.ServerConnectionError: ("503", "Service connection error"), +} +DENDRITE_DEFAULT_ERROR = ("422", "Failed to parse response") + + +def event_loop_is_running(): + try: + asyncio.get_running_loop() + return True + except RuntimeError: + return False + + +class DendriteMixin: + """ + The Dendrite class represents the abstracted implementation of a network client module. + + In the brain analogy, dendrites receive signals + from other neurons (in this case, network servers or axons), and the Dendrite class here is designed + to send requests to those endpoint to receive inputs. + + This class includes a wallet or keypair used for signing messages, and methods for making + HTTP requests to the network servers. It also provides functionalities such as logging + network requests and processing server responses. + + Args: + keypair (Option[Union[bittensor_wallet.Wallet, bittensor_wallet.Keypair]]): The wallet or keypair used for + signing messages. + external_ip (str): The external IP address of the local system. + synapse_history (list): A list of Synapse objects representing the historical responses. + + Methods: + __str__(): Returns a string representation of the Dendrite object. + __repr__(): Returns a string representation of the Dendrite object, acting as a fallback for __str__(). + query(self, *args, **kwargs) -> Union[Synapse, list[Synapse]]: Makes synchronous requests to one or multiple + target Axons and returns responses. + forward(self, axons, synapse=Synapse(), timeout=12, deserialize=True, run_async=True, streaming=False) -> + Synapse: Asynchronously sends requests to one or multiple Axons and collates their responses. + call(self, target_axon, synapse=Synapse(), timeout=12.0, deserialize=True) -> Synapse: Asynchronously sends a + request to a specified Axon and processes the response. + call_stream(self, target_axon, synapse=Synapse(), timeout=12.0, deserialize=True) -> + AsyncGenerator[Synapse, None]: Sends a request to a specified Axon and yields an AsyncGenerator that + contains streaming response chunks before finally yielding the filled Synapse as the final element. + preprocess_synapse_for_request(self, target_axon_info, synapse, timeout=12.0) -> Synapse: Preprocesses the + synapse for making a request, including building headers and signing. + process_server_response(self, server_response, json_response, local_synapse): Processes the server response, + updates the local synapse state, and merges headers. + close_session(self): Synchronously closes the internal aiohttp client session. + aclose_session(self): Asynchronously closes the internal aiohttp client session. + + NOTE: + When working with async `aiohttp `_ client sessions, it is recommended to use a context manager. + + Example with a context manager:: + + async with dendrite(wallet = bittensor_wallet.Wallet()) as d: + print(d) + d( ) # ping axon + d( [] ) # ping multiple + d( Axon(), Synapse ) + + However, you are able to safely call :func:`dendrite.query()` without a context manager in a synchronous setting. + + Example without a context manager:: + + d = dendrite(wallet = bittensor_wallet.Wallet() ) + print(d) + d( ) # ping axon + d( [] ) # ping multiple + d( bittensor.core.axon.Axon, bittensor.core.synapse.Synapse ) + """ + + def __init__(self, wallet: Optional[Union["Wallet", "Keypair"]] = None): + """ + Initializes the Dendrite object, setting up essential properties. + + Args: + wallet (Optional[Union[bittensor_wallet.Wallet, bittensor_wallet.Keypair]]): The user's wallet or keypair + used for signing messages. Defaults to ``None``, in which case a new + :func:`bittensor_wallet.Wallet().hotkey` is generated and used. + """ + # Initialize the parent class + super(DendriteMixin, self).__init__() + + # Unique identifier for the instance + self.uuid = str(uuid.uuid1()) + + # Get the external IP + self.external_ip = networking.get_external_ip() + + # If a wallet or keypair is provided, use its hotkey. If not, generate a new one. + self.keypair = ( + wallet.hotkey if isinstance(wallet, Wallet) else wallet + ) or Wallet().hotkey + + self.synapse_history: list = [] + + self._session: Optional[aiohttp.ClientSession] = None + + @property + async def session(self) -> aiohttp.ClientSession: + """ + An asynchronous property that provides access to the internal `aiohttp `_ + client session. + + This property ensures the management of HTTP connections in an efficient way. It lazily + initializes the `aiohttp.ClientSession `_ + on its first use. The session is then reused for subsequent HTTP requests, offering performance benefits by + reusing underlying connections. + + This is used internally by the dendrite when querying axons, and should not be used directly + unless absolutely necessary for your application. + + Returns: + aiohttp.ClientSession: The active `aiohttp `_ client session instance. + If no session exists, a new one is created and returned. This session is used for asynchronous HTTP requests + within the dendrite, adhering to the async nature of the network interactions in the Bittensor framework. + + Example usage:: + + import bittensor # Import bittensor + wallet = bittensor.Wallet( ... ) # Initialize a wallet + dendrite = bittensor.Dendrite(wallet=wallet) # Initialize a dendrite instance with the wallet + + async with (await dendrite.session).post( # Use the session to make an HTTP POST request + url, # URL to send the request to + headers={...}, # Headers dict to be sent with the request + json={...}, # JSON body data to be sent with the request + timeout=10, # Timeout duration in seconds + ) as response: + json_response = await response.json() # Extract the JSON response from the server + + """ + if self._session is None: + self._session = aiohttp.ClientSession() + return self._session + + def close_session(self, using_new_loop: bool = False): + """ + Closes the internal `aiohttp `_ client session synchronously. + + This method ensures the proper closure and cleanup of the aiohttp client session, releasing any + resources like open connections and internal buffers. It is crucial for preventing resource leakage + and should be called when the dendrite instance is no longer in use, especially in synchronous contexts. + + Arguments: + using_new_loop: A flag to determine whether this has been called with a new event loop rather than + the default. This will indicate whether to close this event loop at the end of this call. + + Note: + This method utilizes asyncio's event loop to close the session asynchronously from a synchronous context. + It is advisable to use this method only when asynchronous context management is not feasible. + + Usage: + When finished with dendrite in a synchronous context + :func:`dendrite_instance.close_session()`. + """ + if event_loop_is_running(): + warnings.warn( + "You are calling this from an already-running event loop. " + "You should instead use `Dendrite.aclose_session`. This will not work within a coroutine in version 9.0", + category=DeprecationWarning, + ) + if self._session: + loop = asyncio.get_event_loop() + loop.run_until_complete(self._session.close()) + if using_new_loop: + loop.close() + self._session = None + + async def aclose_session(self): + """ + Asynchronously closes the internal `aiohttp `_ client session. + + This method is the asynchronous counterpart to the :func:`close_session` method. It should be used in + asynchronous contexts to ensure that the aiohttp client session is closed properly. The method + releases resources associated with the session, such as open connections and internal buffers, + which is essential for resource management in asynchronous applications. + + Example: + Usage:: + When finished with dendrite in an asynchronous context + await :func:`dendrite_instance.aclose_session()`. + + Example: + Usage:: + async with dendrite_instance: + # Operations using dendrite + pass + # The session will be closed automatically after the above block + """ + if self._session: + await self._session.close() + self._session = None + + def _get_endpoint_url(self, target_axon, request_name): + """ + Constructs the endpoint URL for a network request to a target axon. + + This internal method generates the full HTTP URL for sending a request to the specified axon. The + URL includes the IP address and port of the target axon, along with the specific request name. It + differentiates between requests to the local system (using '0.0.0.0') and external systems. + + Args: + target_axon: The target axon object containing IP and port information. + request_name: The specific name of the request being made. + + Returns: + str: A string representing the complete HTTP URL for the request. + """ + endpoint = ( + f"0.0.0.0:{str(target_axon.port)}" + if target_axon.ip == str(self.external_ip) + else f"{target_axon.ip}:{str(target_axon.port)}" + ) + return f"http://{endpoint}/{request_name}" + + def log_exception(self, exception: Exception): + """ + Logs an exception with a unique identifier. + + This method generates a unique UUID for the error, extracts the error type, + and logs the error message using Bittensor's logging system. + + Args: + exception (Exception): The exception object to be logged. + + Returns: + None + """ + error_id = str(uuid.uuid4()) + error_type = exception.__class__.__name__ + if isinstance(exception, (aiohttp.ClientOSError, asyncio.TimeoutError)): + logging.debug(f"{error_type}#{error_id}: {exception}") + else: + logging.error(f"{error_type}#{error_id}: {exception}") + + def process_error_message( + self, + synapse: Union["Synapse", "StreamingSynapse"], + request_name: str, + exception: Exception, + ) -> Union["Synapse", "StreamingSynapse"]: + """ + Handles exceptions that occur during network requests, updating the synapse with appropriate status codes and messages. + + This method interprets different types of exceptions and sets the corresponding status code and + message in the synapse object. It covers common network errors such as connection issues and timeouts. + + Args: + synapse (bittensor.core.synapse.Synapse): The synapse object associated with the request. + request_name (str): The name of the request during which the exception occurred. + exception (Exception): The exception object caught during the request. + + Returns: + Synapse (bittensor.core.synapse.Synapse): The updated synapse object with the error status code and message. + + Note: + This method updates the synapse object in-place. + """ + + self.log_exception(exception) + + error_info = DENDRITE_ERROR_MAPPING.get(type(exception), DENDRITE_DEFAULT_ERROR) + status_code, status_message = error_info + + if status_code: + synapse.dendrite.status_code = status_code # type: ignore + elif isinstance(exception, aiohttp.ClientResponseError): + synapse.dendrite.status_code = str(exception.code) # type: ignore + + message = f"{status_message}: {str(exception)}" + if isinstance(exception, aiohttp.ClientConnectorError): + message = f"{status_message} at {synapse.axon.ip}:{synapse.axon.port}/{request_name}" # type: ignore + elif isinstance(exception, asyncio.TimeoutError): + message = f"{status_message} after {synapse.timeout} seconds" + + synapse.dendrite.status_message = message # type: ignore + + return synapse + + def _log_outgoing_request(self, synapse: "Synapse"): + """ + Logs information about outgoing requests for debugging purposes. + + This internal method logs key details about each outgoing request, including the size of the + request, the name of the synapse, the axon's details, and a success indicator. This information + is crucial for monitoring and debugging network activity within the Bittensor network. + + To turn on debug messages, set the environment variable BITTENSOR_DEBUG to ``1``, or call the bittensor debug + method like so:: + + Example:: + + import bittensor + bittensor.debug() + + Args: + synapse (bittensor.core.synapse.Synapse): The synapse object representing the request being sent. + """ + if synapse.axon is not None: + logging.trace( + f"dendrite | --> | {synapse.get_total_size()} B | {synapse.name} | {synapse.axon.hotkey} | {synapse.axon.ip}:{str(synapse.axon.port)} | 0 | Success" + ) + + def _log_incoming_response(self, synapse: "Synapse"): + """ + Logs information about incoming responses for debugging and monitoring. + + Similar to :func:`_log_outgoing_request`, this method logs essential details of the incoming responses, + including the size of the response, synapse name, axon details, status code, and status message. + This logging is vital for troubleshooting and understanding the network interactions in Bittensor. + + Args: + synapse (bittensor.core.synapse.Synapse): The synapse object representing the received response. + """ + if synapse.axon is not None and synapse.dendrite is not None: + logging.trace( + f"dendrite | <-- | {synapse.get_total_size()} B | {synapse.name} | {synapse.axon.hotkey} | {synapse.axon.ip}:{str(synapse.axon.port)} | {synapse.dendrite.status_code} | {synapse.dendrite.status_message}" + ) + + async def aquery(self, *args, **kwargs): + result = await self.forward(*args, **kwargs) + if self._session: + await self._session.close() + return result + + def query( + self, *args, **kwargs + ) -> list[Union["AsyncGenerator[Any, Any]", "Synapse", "StreamingSynapse"]]: + """ + Makes a synchronous request to multiple target Axons and returns the server responses. + + Cleanup is automatically handled and sessions are closed upon completed requests. + + Args: + axons (Union[list[Union[bittensor.core.chain_data.axon_info.AxonInfo, 'bittensor.core.axon.Axon']], + Union['bittensor.core.chain_data.axon_info.AxonInfo', 'bittensor.core.axon.Axon']]): The list of target + Axon information. + synapse (Optional[bittensor.core.synapse.Synapse]): The Synapse object. Defaults to :func:`Synapse()`. + timeout (Optional[float]): The request timeout duration in seconds. Defaults to ``12.0`` seconds. + + Returns: + Union[bittensor.core.synapse.Synapse, list[bittensor.core.synapse.Synapse]]: If a single target axon is + provided, returns the response from that axon. If multiple target axons are provided, returns a list of + responses from all target axons. + """ + if event_loop_is_running(): + warnings.warn( + "You are calling this from an already-running event loop. " + "You should instead use `Dendrite.aquery`. This will not work within a coroutine in version 9.0", + category=DeprecationWarning, + ) + result = None + use_new_loop = False + try: + loop = asyncio.get_event_loop() + result = loop.run_until_complete(self.forward(*args, **kwargs)) + except Exception as e: + logging.debug( + f"Exception encountered while running Dendrite.query initially: {e}" + ) + new_loop = asyncio.new_event_loop() + asyncio.set_event_loop(new_loop) + result = new_loop.run_until_complete(self.forward(*args, **kwargs)) + use_new_loop = True + finally: + self.close_session(using_new_loop=use_new_loop) + return result # type: ignore + + async def forward( + self, + axons: Union[list[Union["AxonInfo", "Axon"]], Union["AxonInfo", "Axon"]], + synapse: "Synapse" = Synapse(), + timeout: float = 12, + deserialize: bool = True, + run_async: bool = True, + streaming: bool = False, + ) -> list[Union["AsyncGenerator[Any, Any]", "Synapse", "StreamingSynapse"]]: + """ + Asynchronously sends requests to one or multiple Axons and collates their responses. + + This function acts as a bridge for sending multiple requests concurrently or sequentially + based on the provided parameters. It checks the type of the target Axons, preprocesses + the requests, and then sends them off. After getting the responses, it processes and + collates them into a unified format. + + When querying an Axon that sends a single response, this function returns a Synapse object + containing the response data. If multiple Axons are queried, a list of Synapse objects is + returned, each containing the response from the corresponding Axon. + + For example:: + + ... + import bittensor + wallet = bittensor.Wallet() # Initialize a wallet + synapse = bittensor.Synapse(...) # Create a synapse object that contains query data + dendrite = bittensor.Dendrite(wallet = wallet) # Initialize a dendrite instance + netuid = ... # Provide subnet ID + metagraph = bittensor.Metagraph(netuid) # Initialize a metagraph instance + axons = metagraph.axons # Create a list of axons to query + responses = await dendrite(axons, synapse) # Send the query to all axons and await the responses + + When querying an Axon that sends back data in chunks using the Dendrite, this function + returns an AsyncGenerator that yields each chunk as it is received. The generator can be + iterated over to process each chunk individually. + + For example:: + + ... + dendrite = bittensor.Dendrite(wallet = wallet) + async for chunk in dendrite.forward(axons, synapse, timeout, deserialize, run_async, streaming): + # Process each chunk here + print(chunk) + + Args: + axons (Union[list[Union[bittensor.core.chain_data.axon_info.AxonInfo, bittensor.core.axon.Axon]], + Union[bittensor.core.chain_data.axon_info.AxonInfo, bittensor.core.axon.Axon]]): The target Axons to + send requests to. Can be a single Axon or a list of Axons. + synapse (bittensor.core.synapse.Synapse): The Synapse object encapsulating the data. Defaults to a new + :func:`Synapse` instance. + timeout (float): Maximum duration to wait for a response from an Axon in seconds. Defaults to ``12.0``. + deserialize (bool): Determines if the received response should be deserialized. Defaults to ``True``. + run_async (bool): If ``True``, sends requests concurrently. Otherwise, sends requests sequentially. + Defaults to ``True``. + streaming (bool): Indicates if the response is expected to be in streaming format. Defaults to ``False``. + + Returns: + Union[AsyncGenerator, bittensor.core.synapse.Synapse, list[bittensor.core.synapse.Synapse]]: If a single + `Axon` is targeted, returns its response. + If multiple Axons are targeted, returns a list of their responses. + """ + is_list = True + # If a single axon is provided, wrap it in a list for uniform processing + if not isinstance(axons, list): + is_list = False + axons = [axons] + + # Check if synapse is an instance of the StreamingSynapse class or if streaming flag is set. + is_streaming_subclass = issubclass(synapse.__class__, StreamingSynapse) + if streaming != is_streaming_subclass: + logging.warning( + f"Argument streaming is {streaming} while issubclass(synapse, StreamingSynapse) is {synapse.__class__.__name__}. This may cause unexpected behavior." + ) + streaming = is_streaming_subclass or streaming + + async def query_all_axons( + is_stream: bool, + ) -> Union["AsyncGenerator[Any, Any]", "Synapse", "StreamingSynapse"]: + """ + Handles the processing of requests to all targeted axons, accommodating both streaming and non-streaming responses. + + This function manages the concurrent or sequential dispatch of requests to a list of axons. + It utilizes the ``is_stream`` parameter to determine the mode of response handling (streaming + or non-streaming). For each axon, it calls ``single_axon_response`` and aggregates the responses. + + Args: + is_stream (bool): Flag indicating whether the axon responses are expected to be streamed. + If ``True``, responses are handled in streaming mode. + + Returns: + list[Union[AsyncGenerator, bittensor.core.synapse.Synapse, bittensor.core.stream.StreamingSynapse]]: + A list containing the responses from each axon. The type of each response depends on the streaming + mode and the type of synapse used. + """ + + async def single_axon_response( + target_axon: Union["AxonInfo", "Axon"], + ) -> Union["AsyncGenerator[Any, Any]", "Synapse", "StreamingSynapse"]: + """ + Manages the request and response process for a single axon, supporting both streaming and non-streaming modes. + + This function is responsible for initiating a request to a single axon. Depending on the ``is_stream`` + flag, it either uses ``call_stream`` for streaming responses or ``call`` for standard responses. The + function handles the response processing, catering to the specifics of streaming or non-streaming data. + + Args: + target_axon (Union[bittensor.core.chain_data.axon_info.AxonInfo, bittensor.core.axon.Axon): The + target axon object to which the request is to be sent. This object contains the necessary + information like IP address and port to formulate the request. + + Returns: + Union[AsyncGenerator, bittensor.core.synapse.Synapse, bittensor.core.stream.StreamingSynapse]: The + response from the targeted axon. In streaming mode, an AsyncGenerator is returned, yielding data + chunks. In non-streaming mode, a Synapse or StreamingSynapse object is returned containing the + response. + """ + if is_stream: + # If in streaming mode, return the async_generator + return self.call_stream( + target_axon=target_axon, + synapse=synapse.model_copy(), # type: ignore + timeout=timeout, + deserialize=deserialize, + ) + else: + # If not in streaming mode, simply call the axon and get the response. + return await self.call( + target_axon=target_axon, + synapse=synapse.model_copy(), # type: ignore + timeout=timeout, + deserialize=deserialize, + ) + + # If run_async flag is False, get responses one by one. + if not run_async: + return [ + await single_axon_response(target_axon) for target_axon in axons + ] # type: ignore + # If run_async flag is True, get responses concurrently using asyncio.gather(). + return await asyncio.gather( + *(single_axon_response(target_axon) for target_axon in axons) + ) # type: ignore + + # Get responses for all axons. + responses = await query_all_axons(streaming) + # Return the single response if only one axon was targeted, else return all responses + return responses[0] if len(responses) == 1 and not is_list else responses # type: ignore + + async def call( + self, + target_axon: Union["AxonInfo", "Axon"], + synapse: "Synapse" = Synapse(), + timeout: float = 12.0, + deserialize: bool = True, + ) -> "Synapse": + """ + Asynchronously sends a request to a specified Axon and processes the response. + + This function establishes a connection with a specified Axon, sends the encapsulated data through the Synapse + object, waits for a response, processes it, and then returns the updated Synapse object. + + Args: + target_axon (Union[bittensor.core.chain_data.axon_info.AxonInfo, bittensor.core.axon.Axon]): The target Axon + to send the request to. + synapse (bittensor.core.synapse.Synapse): The Synapse object encapsulating the data. Defaults to a new + :func:`Synapse` instance. + timeout (float): Maximum duration to wait for a response from the Axon in seconds. Defaults to ``12.0``. + deserialize (bool): Determines if the received response should be deserialized. Defaults to ``True``. + + Returns: + bittensor.core.synapse.Synapse: The Synapse object, updated with the response data from the Axon. + """ + + # Record start time + start_time = time.time() + target_axon = ( + target_axon.info() if isinstance(target_axon, Axon) else target_axon + ) + + # Build request endpoint from the synapse class + request_name = synapse.__class__.__name__ + url = self._get_endpoint_url(target_axon, request_name=request_name) + + # Preprocess synapse for making a request + synapse = self.preprocess_synapse_for_request(target_axon, synapse, timeout) + + try: + # Log outgoing request + self._log_outgoing_request(synapse) + + # Make the HTTP POST request + async with (await self.session).post( + url=url, + headers=synapse.to_headers(), + json=synapse.model_dump(), + timeout=aiohttp.ClientTimeout(total=timeout), + ) as response: + # Extract the JSON response from the server + json_response = await response.json() + # Process the server response and fill synapse + self.process_server_response(response, json_response, synapse) + + # Set process time and log the response + synapse.dendrite.process_time = str(time.time() - start_time) # type: ignore + + except Exception as e: + synapse = self.process_error_message(synapse, request_name, e) + + finally: + self._log_incoming_response(synapse) + + # Log synapse event history + self.synapse_history.append(Synapse.from_headers(synapse.to_headers())) + + # Return the updated synapse object after deserializing if requested + return synapse.deserialize() if deserialize else synapse + + async def call_stream( + self, + target_axon: Union["AxonInfo", "Axon"], + synapse: "StreamingSynapse" = Synapse(), # type: ignore + timeout: float = 12.0, + deserialize: bool = True, + ) -> "AsyncGenerator[Any, Any]": + """ + Sends a request to a specified Axon and yields streaming responses. + + Similar to ``call``, but designed for scenarios where the Axon sends back data in + multiple chunks or streams. The function yields each chunk as it is received. This is + useful for processing large responses piece by piece without waiting for the entire + data to be transmitted. + + Args: + target_axon (Union[bittensor.core.chain_data.axon_info.AxonInfo, bittensor.core.axon.Axon]): The target Axon + to send the request to. + synapse (bittensor.core.synapse.Synapse): The Synapse object encapsulating the data. Defaults to a new + :func:`Synapse` instance. + timeout (float): Maximum duration to wait for a response (or a chunk of the response) from the Axon in + seconds. Defaults to ``12.0``. + deserialize (bool): Determines if each received chunk should be deserialized. Defaults to ``True``. + + Yields: + object: Each yielded object contains a chunk of the arbitrary response data from the Axon. + bittensor.core.synapse.Synapse: After the AsyncGenerator has been exhausted, yields the final filled Synapse. + """ + + # Record start time + start_time = time.time() + target_axon = ( + target_axon.info() if isinstance(target_axon, Axon) else target_axon + ) + + # Build request endpoint from the synapse class + request_name = synapse.__class__.__name__ + endpoint = ( + f"0.0.0.0:{str(target_axon.port)}" + if target_axon.ip == str(self.external_ip) + else f"{target_axon.ip}:{str(target_axon.port)}" + ) + url = f"http://{endpoint}/{request_name}" + + # Preprocess synapse for making a request + synapse = self.preprocess_synapse_for_request(target_axon, synapse, timeout) # type: ignore + + try: + # Log outgoing request + self._log_outgoing_request(synapse) + + # Make the HTTP POST request + async with (await self.session).post( + url, + headers=synapse.to_headers(), + json=synapse.model_dump(), + timeout=aiohttp.ClientTimeout(total=timeout), + ) as response: + # Use synapse subclass' process_streaming_response method to yield the response chunks + async for chunk in synapse.process_streaming_response(response): # type: ignore + yield chunk # Yield each chunk as it's processed + json_response = synapse.extract_response_json(response) + + # Process the server response + self.process_server_response(response, json_response, synapse) + + # Set process time and log the response + synapse.dendrite.process_time = str(time.time() - start_time) # type: ignore + + except Exception as e: + synapse = self.process_error_message(synapse, request_name, e) # type: ignore + + finally: + self._log_incoming_response(synapse) + + # Log synapse event history + self.synapse_history.append(Synapse.from_headers(synapse.to_headers())) + + # Return the updated synapse object after deserializing if requested + if deserialize: + yield synapse.deserialize() + else: + yield synapse + + def preprocess_synapse_for_request( + self, + target_axon_info: "AxonInfo", + synapse: "Synapse", + timeout: float = 12.0, + ) -> "Synapse": + """ + Preprocesses the synapse for making a request. This includes building headers for Dendrite and Axon and signing + the request. + + Args: + target_axon_info (bittensor.core.chain_data.axon_info.AxonInfo): The target axon information. + synapse (bittensor.core.synapse.Synapse): The synapse object to be preprocessed. + timeout (float): The request timeout duration in seconds. Defaults to ``12.0`` seconds. + + Returns: + bittensor.core.synapse.Synapse: The preprocessed synapse. + """ + # Set the timeout for the synapse + synapse.timeout = timeout + synapse.dendrite = TerminalInfo( + ip=self.external_ip, + version=version_as_int, + nonce=time.time_ns(), + uuid=self.uuid, + hotkey=self.keypair.ss58_address, + ) + + # Build the Axon headers using the target axon's details + synapse.axon = TerminalInfo( + ip=target_axon_info.ip, + port=target_axon_info.port, + hotkey=target_axon_info.hotkey, + ) + + # Sign the request using the dendrite, axon info, and the synapse body hash + message = f"{synapse.dendrite.nonce}.{synapse.dendrite.hotkey}.{synapse.axon.hotkey}.{synapse.dendrite.uuid}.{synapse.body_hash}" + synapse.dendrite.signature = f"0x{self.keypair.sign(message).hex()}" + + return synapse + + def process_server_response( + self, + server_response: "aiohttp.ClientResponse", + json_response: dict, + local_synapse: "Synapse", + ): + """ + Processes the server response, updates the local synapse state with the server's state and merges headers set + by the server. + + Args: + server_response (object): The `aiohttp `_ response object from the server. + json_response (dict): The parsed JSON response from the server. + local_synapse (bittensor.core.synapse.Synapse): The local synapse object to be updated. + + Raises: + None: But errors in attribute setting are silently ignored. + """ + # Check if the server responded with a successful status code + if server_response.status == 200: + # If the response is successful, overwrite local synapse state with + # server's state only if the protocol allows mutation. To prevent overwrites, + # the protocol must set Frozen = True + server_synapse = local_synapse.__class__(**json_response) + for key in local_synapse.model_dump().keys(): + try: + # Set the attribute in the local synapse from the corresponding + # attribute in the server synapse + setattr(local_synapse, key, getattr(server_synapse, key)) + except Exception: + # Ignore errors during attribute setting + pass + else: + # If the server responded with an error, update the local synapse state + if local_synapse.axon is None: + local_synapse.axon = TerminalInfo() + local_synapse.axon.status_code = server_response.status + local_synapse.axon.status_message = json_response.get("message") + + # Extract server headers and overwrite None values in local synapse headers + server_headers = Synapse.from_headers(server_response.headers) # type: ignore + + # Merge dendrite headers + local_synapse.dendrite.__dict__.update( + { + **local_synapse.dendrite.model_dump(exclude_none=True), # type: ignore + **server_headers.dendrite.model_dump(exclude_none=True), # type: ignore + } + ) + + # Merge axon headers + local_synapse.axon.__dict__.update( + { + **local_synapse.axon.model_dump(exclude_none=True), # type: ignore + **server_headers.axon.model_dump(exclude_none=True), # type: ignore + } + ) + + # Update the status code and status message of the dendrite to match the axon + local_synapse.dendrite.status_code = local_synapse.axon.status_code # type: ignore + local_synapse.dendrite.status_message = local_synapse.axon.status_message # type: ignore + + def __str__(self) -> str: + """ + Returns a string representation of the Dendrite object. + + Returns: + str: The string representation of the Dendrite object in the format :func:`dendrite()`. + """ + return f"dendrite({self.keypair.ss58_address})" + + def __repr__(self) -> str: + """ + Returns a string representation of the Dendrite object, acting as a fallback for :func:`__str__()`. + + Returns: + str: The string representation of the Dendrite object in the format :func:`dendrite()`. + """ + return self.__str__() + + async def __aenter__(self): + """ + Asynchronous context manager entry method. + + Enables the use of the ``async with`` statement with the Dendrite instance. When entering the context, the + current instance of the class is returned, making it accessible within the asynchronous context. + + Returns: + Dendrite: The current instance of the Dendrite class. + + Usage:: + async with Dendrite() as dendrite: + await dendrite.some_async_method() + """ + return self + + async def __aexit__(self, exc_type, exc_value, traceback): + """ + Asynchronous context manager exit method. + + Ensures proper cleanup when exiting the ``async with`` context. This method will close the + `aiohttp `_ client session asynchronously, releasing any tied resources. + + Args: + exc_type (Type[BaseException]): The type of exception that was raised. + exc_value (BaseException): The instance of exception that was raised. + traceback (TracebackType): A traceback object encapsulating the call stack at the point where the exception + was raised. + + Usage:: + import bittensor + + wallet = bittensor.Wallet() + async with bittensor.Dendrite(wallet=wallet) as dendrite: + await dendrite.some_async_method() + + Note: + This automatically closes the session by calling :func:`__aexit__` after the context closes. + """ + await self.aclose_session() + + def __del__(self): + """ + Dendrite destructor. + + This method is invoked when the Dendrite instance is about to be destroyed. The destructor ensures that the + aiohttp client session is closed before the instance is fully destroyed, releasing any remaining resources. + + Note: + Relying on the destructor for cleanup can be unpredictable. It is recommended to explicitly close sessions + using the provided methods or the ``async with`` context manager. + + Usage:: + + dendrite = Dendrite() + # ... some operations ... + del dendrite # This will implicitly invoke the __del__ method and close the session. + """ + try: + self.close_session() + except RuntimeError: + if self._session: + logging.debug( + "A Dendrite session was unable to be closed during garbage-collection of the Dendrite object. This " + "usually indicates that you were not using the async context manager." + ) + + +# For back-compatibility with torch +BaseModel: Union["torch.nn.Module", object] = torch.nn.Module if use_torch() else object + + +class Dendrite(DendriteMixin, BaseModel): # type: ignore + def __init__(self, wallet: Optional[Union["Wallet", "Keypair"]] = None): + if use_torch(): + torch.nn.Module.__init__(self) + DendriteMixin.__init__(self, wallet) + + +if not use_torch(): + + async def call(self, *args, **kwargs): + return await self.forward(*args, **kwargs) + + Dendrite.__call__ = call diff --git a/bittensor/core/errors.py b/bittensor/core/errors.py new file mode 100644 index 0000000000..d4b438bd2f --- /dev/null +++ b/bittensor/core/errors.py @@ -0,0 +1,262 @@ +from typing import Optional, TYPE_CHECKING + +from async_substrate_interface.errors import ( + SubstrateRequestException, + StorageFunctionNotFound, + BlockNotFound, + ExtrinsicNotFound, +) + +if TYPE_CHECKING: + from bittensor.core.synapse import Synapse + +# redundant aliases +SubstrateRequestException = SubstrateRequestException +StorageFunctionNotFound = StorageFunctionNotFound +BlockNotFound = BlockNotFound +ExtrinsicNotFound = ExtrinsicNotFound + + +class _ChainErrorMeta(type): + _exceptions: dict[str, Exception] = {} + + def __new__(mcs, name, bases, attrs): + cls = super().__new__(mcs, name, bases, attrs) + + mcs._exceptions.setdefault(cls.__name__, cls) + + return cls + + @classmethod + def get_exception_class(mcs, exception_name): + return mcs._exceptions[exception_name] + + +class MaxSuccessException(Exception): + """Raised when the POW Solver has reached the max number of successful solutions.""" + + +class MaxAttemptsException(Exception): + """Raised when the POW Solver has reached the max number of attempts.""" + + +class ChainError(SubstrateRequestException, metaclass=_ChainErrorMeta): + """Base error for any chain related errors.""" + + @classmethod + def from_error(cls, error): + try: + error_cls = _ChainErrorMeta.get_exception_class( + error["name"], + ) + except KeyError: + return cls(error) + else: + return error_cls(" ".join(error["docs"])) + + +class ChainConnectionError(ChainError): + """Error for any chain connection related errors.""" + + +class ChainTransactionError(ChainError): + """Error for any chain transaction related errors.""" + + +class DelegateTakeTooHigh(ChainTransactionError): + """ + Delegate take is too high. + """ + + +class DelegateTakeTooLow(ChainTransactionError): + """ + Delegate take is too low. + """ + + +class DuplicateChild(ChainTransactionError): + """ + Duplicate child when setting children. + """ + + +class HotKeyAccountNotExists(ChainTransactionError): + """ + The hotkey does not exist. + """ + + +class IdentityError(ChainTransactionError): + """ + Error raised when an identity transaction fails. + """ + + +class InvalidChild(ChainTransactionError): + """ + Attempting to set an invalid child for a hotkey on a network. + """ + + +class MetadataError(ChainTransactionError): + """ + Error raised when metadata commitment transaction fails. + """ + + +class NominationError(ChainTransactionError): + """ + Error raised when a nomination transaction fails. + """ + + +class NonAssociatedColdKey(ChainTransactionError): + """ + Request to stake, unstake or subscribe is made by a coldkey that is not associated with the hotkey account. + """ + + +class NotEnoughStakeToSetChildkeys(ChainTransactionError): + """ + The parent hotkey doesn't have enough own stake to set childkeys. + """ + + +class NotRegisteredError(ChainTransactionError): + """ + Error raised when a neuron is not registered, and the transaction requires it to be. + """ + + +class ProportionOverflow(ChainTransactionError): + """ + Proportion overflow when setting children. + """ + + +class RegistrationError(ChainTransactionError): + """ + Error raised when a neuron registration transaction fails. + """ + + +class RegistrationNotPermittedOnRootSubnet(ChainTransactionError): + """ + Operation is not permitted on the root subnet. + """ + + +class StakeError(ChainTransactionError): + """ + Error raised when a stake transaction fails. + """ + + +class NotDelegateError(StakeError): + """ + Error raised when a hotkey you are trying to stake to is not a delegate. + """ + + +class SubNetworkDoesNotExist(ChainTransactionError): + """ + The subnet does not exist. + """ + + +class TakeError(ChainTransactionError): + """ + Error raised when an increase / decrease take transaction fails. + """ + + +class TransferError(ChainTransactionError): + """ + Error raised when a transfer transaction fails. + """ + + +class TooManyChildren(ChainTransactionError): + """ + Too many children MAX 5. + """ + + +class TxRateLimitExceeded(ChainTransactionError): + """ + Default transaction rate limit exceeded. + """ + + +class DelegateTxRateLimitExceeded(TxRateLimitExceeded): + """ + A transactor exceeded the rate limit for delegate transaction. + """ + + +class UnstakeError(ChainTransactionError): + """ + Error raised when an unstake transaction fails. + """ + + +class ChainQueryError(ChainError): + """ + Error for any chain query related errors. + """ + + +class InvalidRequestNameError(Exception): + """This exception is raised when the request name is invalid. Usually indicates a broken URL.""" + + +class SynapseException(Exception): + def __init__( + self, message="Synapse Exception", synapse: Optional["Synapse"] = None + ): + self.message = message + self.synapse = synapse + super().__init__(self.message) + + +class UnknownSynapseError(SynapseException): + """This exception is raised when the request name is not found in the Axon's forward_fns dictionary.""" + + +class SynapseParsingError(Exception): + """This exception is raised when the request headers are unable to be parsed into the synapse type.""" + + +class NotVerifiedException(SynapseException): + """This exception is raised when the request is not verified.""" + + +class BlacklistedException(SynapseException): + """This exception is raised when the request is blacklisted.""" + + +class PriorityException(SynapseException): + """This exception is raised when the request priority is not met.""" + + +class PostProcessException(SynapseException): + """This exception is raised when the response headers cannot be updated.""" + + +class RunException(SynapseException): + """This exception is raised when the requested function cannot be executed. Indicates a server error.""" + + +class InternalServerError(SynapseException): + """This exception is raised when the requested function fails on the server. Indicates a server error.""" + + +class SynapseDendriteNoneException(SynapseException): + def __init__( + self, + message="Synapse Dendrite is None", + synapse: Optional["Synapse"] = None, + ): + self.message = message + super().__init__(self.message, synapse) diff --git a/bittensor/exceptions/handlers/__init__.py b/bittensor/core/extrinsics/__init__.py similarity index 100% rename from bittensor/exceptions/handlers/__init__.py rename to bittensor/core/extrinsics/__init__.py diff --git a/bittensor/subtensor/__init__.py b/bittensor/core/extrinsics/asyncex/__init__.py similarity index 100% rename from bittensor/subtensor/__init__.py rename to bittensor/core/extrinsics/asyncex/__init__.py diff --git a/bittensor/core/extrinsics/asyncex/children.py b/bittensor/core/extrinsics/asyncex/children.py new file mode 100644 index 0000000000..7476ef8fb8 --- /dev/null +++ b/bittensor/core/extrinsics/asyncex/children.py @@ -0,0 +1,158 @@ +from typing import TYPE_CHECKING, Optional +from bittensor.utils import float_to_u64, unlock_key + +if TYPE_CHECKING: + from bittensor_wallet import Wallet + from bittensor.core.async_subtensor import AsyncSubtensor + + +async def set_children_extrinsic( + subtensor: "AsyncSubtensor", + wallet: "Wallet", + hotkey: str, + netuid: int, + children: list[tuple[float, str]], + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> tuple[bool, str]: + """ + Allows a coldkey to set children-keys. + + Parameters: + subtensor: The Subtensor client instance used for blockchain interaction. + wallet: bittensor wallet instance. + hotkey: The ``SS58`` address of the neuron's hotkey. + netuid: The netuid value. + children: A list of children with their proportions. + 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. + + Returns: + Tuple[bool, str]: + - True and a success message if the extrinsic is successfully submitted or processed. + - False and an error message if the submission fails or the wallet cannot be unlocked. + + Raises: + DuplicateChild: There are duplicates in the list of children. + InvalidChild: Child is the hotkey. + NonAssociatedColdKey: The coldkey does not own the hotkey or the child is the same as the hotkey. + NotEnoughStakeToSetChildkeys: Parent key doesn't have minimum own stake. + ProportionOverflow: The sum of the proportions does exceed uint64. + RegistrationNotPermittedOnRootSubnet: Attempting to register a child on the root network. + SubNetworkDoesNotExist: Attempting to register to a non-existent network. + TooManyChildren: Too many children in request. + TxRateLimitExceeded: Hotkey hit the rate limit. + bittensor_wallet.errors.KeyFileError: Failed to decode keyfile data. + bittensor_wallet.errors.PasswordError: Decryption failed or wrong password for decryption provided. + """ + unlock = unlock_key(wallet, raise_error=raise_error) + + if not unlock.success: + return False, unlock.message + + async with subtensor.substrate as substrate: + call = await substrate.compose_call( + call_module="SubtensorModule", + call_function="set_children", + call_params={ + "children": [ + ( + float_to_u64(proportion), + child_hotkey, + ) + for proportion, child_hotkey in children + ], + "hotkey": hotkey, + "netuid": netuid, + }, + ) + + success, message = 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 wait_for_finalization and not wait_for_inclusion: + return True, message + + if success: + return True, "Success with `set_children_extrinsic` response." + + return True, message + + +async def root_set_pending_childkey_cooldown_extrinsic( + subtensor: "AsyncSubtensor", + wallet: "Wallet", + cooldown: int, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> tuple[bool, str]: + """ + Allows a root coldkey to set children-keys. + + Parameters: + subtensor: The Subtensor client instance used for blockchain interaction. + wallet: The wallet used to sign the extrinsic (must be unlocked). + cooldown: The cooldown period in blocks. + 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. + + Returns: + Tuple[bool, str]: + - True and a success message if the extrinsic is successfully submitted or processed. + - False and an error message if the submission fails or the wallet cannot be unlocked. + """ + unlock = unlock_key(wallet) + + if not unlock.success: + return False, unlock.message + + async with subtensor.substrate as substrate: + call = await substrate.compose_call( + call_module="SubtensorModule", + call_function="set_pending_childkey_cooldown", + call_params={"cooldown": cooldown}, + ) + + sudo_call = await substrate.compose_call( + call_module="Sudo", + call_function="sudo", + call_params={"call": call}, + ) + + success, message = await subtensor.sign_and_send_extrinsic( + call=sudo_call, + wallet=wallet, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + if not wait_for_finalization and not wait_for_inclusion: + return True, message + + if success: + return ( + True, + "Success with `root_set_pending_childkey_cooldown_extrinsic` response.", + ) + + return True, message diff --git a/bittensor/core/extrinsics/asyncex/commit_reveal.py b/bittensor/core/extrinsics/asyncex/commit_reveal.py new file mode 100644 index 0000000000..fd1bd00f53 --- /dev/null +++ b/bittensor/core/extrinsics/asyncex/commit_reveal.py @@ -0,0 +1,116 @@ +"""This module provides async functionality for commit reveal in the Bittensor network.""" + +from typing import Optional, Union, TYPE_CHECKING + +import numpy as np +from bittensor_drand import get_encrypted_commit +from numpy.typing import NDArray + +from bittensor.core.settings import version_as_int +from bittensor.utils.btlogging import logging +from bittensor.utils.weight_utils import convert_and_normalize_weights_and_uids + +if TYPE_CHECKING: + from bittensor_wallet import Wallet + from bittensor.core.async_subtensor import AsyncSubtensor + from bittensor.utils.registration import torch + + +async def commit_reveal_extrinsic( + subtensor: "AsyncSubtensor", + wallet: "Wallet", + netuid: int, + uids: Union[NDArray[np.int64], "torch.LongTensor", list], + weights: Union[NDArray[np.float32], "torch.FloatTensor", list], + block_time: Union[int, float] = 12.0, + commit_reveal_version: int = 4, + version_key: int = version_as_int, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = False, + wait_for_finalization: bool = False, +) -> tuple[bool, str]: + """ + Commits and reveals weights for a given subtensor and wallet with provided uids and weights. + + Parameters: + subtensor: The Subtensor instance. + wallet: The wallet to use for committing and revealing. + netuid: The id of the network. + uids: The uids to commit. + weights: The weights associated with the uids. + block_time: The number of seconds for block duration. + commit_reveal_version: The version of the chain commit-reveal protocol to use. + version_key: The version key to use for committing and revealing. + 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. + + Returns: + Tuple[bool, str]: + - True and a success message if the extrinsic is successfully submitted or processed. + - False and an error message if the submission fails or the wallet cannot be unlocked. + """ + try: + uids, weights = convert_and_normalize_weights_and_uids(uids, weights) + + current_block = await subtensor.substrate.get_block(None) + subnet_hyperparameters = await subtensor.get_subnet_hyperparameters( + netuid, block_hash=current_block["header"]["hash"] + ) + tempo = subnet_hyperparameters.tempo + subnet_reveal_period_epochs = subnet_hyperparameters.commit_reveal_period + + # Encrypt `commit_hash` with t-lock and `get reveal_round` + commit_for_reveal, reveal_round = get_encrypted_commit( + uids=uids, + weights=weights, + version_key=version_key, + tempo=tempo, + current_block=current_block["header"]["number"], + netuid=netuid, + subnet_reveal_period_epochs=subnet_reveal_period_epochs, + block_time=block_time, + hotkey=wallet.hotkey.public_key, + ) + + logging.info( + f"Committing weights hash [blue]{commit_for_reveal.hex()}[/blue] for subnet #[blue]{netuid}[/blue] with " + f"reveal round [blue]{reveal_round}[/blue]..." + ) + + call = await subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="commit_timelocked_weights", + call_params={ + "netuid": netuid, + "commit": commit_for_reveal, + "reveal_round": reveal_round, + "commit_reveal_version": commit_reveal_version, + }, + ) + success, message = await subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + sign_with="hotkey", + period=period, + raise_error=raise_error, + ) + + if not success: + logging.error(message) + return False, message + + logging.success( + f"[green]Finalized![/green] Weights committed with reveal round [blue]{reveal_round}[/blue]." + ) + return True, f"reveal_round:{reveal_round}" + + except Exception as e: + logging.error(f":cross_mark: [red]Failed. Error:[/red] {e}") + return False, str(e) diff --git a/bittensor/core/extrinsics/asyncex/liquidity.py b/bittensor/core/extrinsics/asyncex/liquidity.py new file mode 100644 index 0000000000..fc98f46631 --- /dev/null +++ b/bittensor/core/extrinsics/asyncex/liquidity.py @@ -0,0 +1,249 @@ +from typing import Optional, TYPE_CHECKING + +from bittensor.utils import unlock_key +from bittensor.utils.balance import Balance +from bittensor.utils.btlogging import logging +from bittensor.utils.liquidity import price_to_tick + +if TYPE_CHECKING: + from bittensor_wallet import Wallet + from bittensor.core.async_subtensor import AsyncSubtensor + + +async def add_liquidity_extrinsic( + subtensor: "AsyncSubtensor", + wallet: "Wallet", + netuid: int, + liquidity: Balance, + price_low: Balance, + price_high: Balance, + hotkey: Optional[str] = None, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> tuple[bool, str]: + """ + Adds liquidity to the specified price range. + + Parameters: + subtensor: The Subtensor client instance used for blockchain interaction. + wallet: The wallet used to sign the extrinsic (must be unlocked). + netuid: The UID of the target subnet for which the call is being initiated. + liquidity: The amount of liquidity to be added. + price_low: The lower bound of the price tick range. + price_high: The upper bound of the price tick range. + hotkey: The hotkey with staked TAO in Alpha. If not passed then the wallet hotkey is used. + 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. + + Returns: + Tuple[bool, str]: + - True and a success message if the extrinsic is successfully submitted or processed. + - False and an error message if the submission fails or the wallet cannot be unlocked. + + Note: Adding is allowed even when user liquidity is enabled in specified subnet. Call + `toggle_user_liquidity_extrinsic` to enable/disable user liquidity. + """ + if not (unlock := unlock_key(wallet)).success: + logging.error(unlock.message) + return False, unlock.message + + tick_low = price_to_tick(price_low.tao) + tick_high = price_to_tick(price_high.tao) + + call = await subtensor.substrate.compose_call( + call_module="Swap", + call_function="add_liquidity", + call_params={ + "hotkey": hotkey or wallet.hotkey.ss58_address, + "netuid": netuid, + "tick_low": tick_low, + "tick_high": tick_high, + "liquidity": liquidity.rao, + }, + ) + + return await subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + use_nonce=True, + period=period, + raise_error=raise_error, + ) + + +async def modify_liquidity_extrinsic( + subtensor: "AsyncSubtensor", + wallet: "Wallet", + netuid: int, + position_id: int, + liquidity_delta: Balance, + hotkey: Optional[str] = None, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> tuple[bool, str]: + """Modifies liquidity in liquidity position by adding or removing liquidity from it. + + Parameters: + subtensor: The Subtensor client instance used for blockchain interaction. + wallet: The wallet used to sign the extrinsic (must be unlocked). + netuid: The UID of the target subnet for which the call is being initiated. + position_id: The id of the position record in the pool. + liquidity_delta: The amount of liquidity to be added or removed (add if positive or remove if negative). + hotkey: The hotkey with staked TAO in Alpha. If not passed then the wallet hotkey is used. Defaults to `None`. + 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. + + Returns: + Tuple[bool, str]: + - True and a success message if the extrinsic is successfully submitted or processed. + - False and an error message if the submission fails or the wallet cannot be unlocked. + + Note: Modifying is allowed even when user liquidity is enabled in specified subnet. + Call `toggle_user_liquidity_extrinsic` to enable/disable user liquidity. + """ + if not (unlock := unlock_key(wallet)).success: + logging.error(unlock.message) + return False, unlock.message + + call = await subtensor.substrate.compose_call( + call_module="Swap", + call_function="modify_position", + call_params={ + "hotkey": hotkey or wallet.hotkey.ss58_address, + "netuid": netuid, + "position_id": position_id, + "liquidity_delta": liquidity_delta.rao, + }, + ) + + return await subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + use_nonce=True, + period=period, + raise_error=raise_error, + ) + + +async def remove_liquidity_extrinsic( + subtensor: "AsyncSubtensor", + wallet: "Wallet", + netuid: int, + position_id: int, + hotkey: Optional[str] = None, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> tuple[bool, str]: + """Remove liquidity and credit balances back to wallet's hotkey stake. + + Parameters: + subtensor: The Subtensor client instance used for blockchain interaction. + wallet: The wallet used to sign the extrinsic (must be unlocked). + netuid: The UID of the target subnet for which the call is being initiated. + position_id: The id of the position record in the pool. + hotkey: The hotkey with staked TAO in Alpha. If not passed then the wallet hotkey is used. Defaults to `None`. + 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. + + Returns: + Tuple[bool, str]: + - True and a success message if the extrinsic is successfully submitted or processed. + - False and an error message if the submission fails or the wallet cannot be unlocked. + + Note: Adding is allowed even when user liquidity is enabled in specified subnet. Call + `toggle_user_liquidity_extrinsic` to enable/disable user liquidity. + """ + if not (unlock := unlock_key(wallet)).success: + logging.error(unlock.message) + return False, unlock.message + + call = await subtensor.substrate.compose_call( + call_module="Swap", + call_function="remove_liquidity", + call_params={ + "hotkey": hotkey or wallet.hotkey.ss58_address, + "netuid": netuid, + "position_id": position_id, + }, + ) + + return await subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + use_nonce=True, + period=period, + raise_error=raise_error, + ) + + +async def toggle_user_liquidity_extrinsic( + subtensor: "AsyncSubtensor", + wallet: "Wallet", + netuid: int, + enable: bool, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> tuple[bool, str]: + """Allow to toggle user liquidity for specified subnet. + + Parameters: + subtensor: The Subtensor client instance used for blockchain interaction. + wallet: The wallet used to sign the extrinsic (must be unlocked). + netuid: The UID of the target subnet for which the call is being initiated. + enable: Boolean indicating whether to enable user liquidity. + 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. + + Returns: + Tuple[bool, str]: + - True and a success message if the extrinsic is successfully submitted or processed. + - False and an error message if the submission fails or the wallet cannot be unlocked. + """ + if not (unlock := unlock_key(wallet)).success: + logging.error(unlock.message) + return False, unlock.message + + call = await subtensor.substrate.compose_call( + call_module="Swap", + call_function="toggle_user_liquidity", + call_params={"netuid": netuid, "enable": enable}, + ) + + return 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, + ) diff --git a/bittensor/core/extrinsics/asyncex/move_stake.py b/bittensor/core/extrinsics/asyncex/move_stake.py new file mode 100644 index 0000000000..a401350251 --- /dev/null +++ b/bittensor/core/extrinsics/asyncex/move_stake.py @@ -0,0 +1,425 @@ +import asyncio +from typing import TYPE_CHECKING, Optional + +from bittensor.utils.balance import Balance +from bittensor.utils.btlogging import logging + +if TYPE_CHECKING: + from bittensor_wallet import Wallet + from bittensor.core.async_subtensor import AsyncSubtensor + + +async def _get_stake_in_origin_and_dest( + subtensor: "AsyncSubtensor", + origin_hotkey_ss58: str, + destination_hotkey_ss58: str, + origin_coldkey_ss58: str, + destination_coldkey_ss58: str, + origin_netuid: int, + destination_netuid: int, +) -> tuple[Balance, Balance]: + """Gets the current stake balances for both origin and destination addresses in their respective subnets.""" + block_hash = await subtensor.substrate.get_chain_head() + stake_in_origin, stake_in_destination = await asyncio.gather( + subtensor.get_stake( + coldkey_ss58=origin_coldkey_ss58, + hotkey_ss58=origin_hotkey_ss58, + netuid=origin_netuid, + block_hash=block_hash, + ), + subtensor.get_stake( + coldkey_ss58=destination_coldkey_ss58, + hotkey_ss58=destination_hotkey_ss58, + netuid=destination_netuid, + block_hash=block_hash, + ), + ) + return stake_in_origin, stake_in_destination + + +async def transfer_stake_extrinsic( + subtensor: "AsyncSubtensor", + wallet: "Wallet", + destination_coldkey_ss58: str, + hotkey_ss58: str, + origin_netuid: int, + destination_netuid: int, + amount: Balance, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> bool: + """ + Transfers stake from one coldkey to another in the Bittensor network. + + Parameters: + subtensor: The subtensor instance to interact with the blockchain. + wallet: The wallet containing the coldkey to authorize the transfer. + destination_coldkey_ss58: SS58 address of the destination coldkey. + hotkey_ss58: SS58 address of the hotkey associated with the stake. + origin_netuid: Network UID of the origin subnet. + destination_netuid: Network UID of the destination subnet. + amount: The amount of stake to transfer as a `Balance` object. + 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. + + Returns: + bool: True if the transfer was successful, False otherwise. + """ + + amount.set_unit(netuid=origin_netuid) + + # Check sufficient stake + stake_in_origin, stake_in_destination = await _get_stake_in_origin_and_dest( + subtensor=subtensor, + origin_hotkey_ss58=hotkey_ss58, + destination_hotkey_ss58=hotkey_ss58, + origin_coldkey_ss58=wallet.coldkeypub.ss58_address, + destination_coldkey_ss58=destination_coldkey_ss58, + origin_netuid=origin_netuid, + destination_netuid=destination_netuid, + ) + if stake_in_origin < amount: + logging.error( + f":cross_mark: [red]Failed[/red]: Insufficient stake in origin hotkey: {hotkey_ss58}. " + f"Stake: {stake_in_origin}, amount: {amount}" + ) + return False + + try: + logging.info( + f"Transferring stake from coldkey [blue]{wallet.coldkeypub.ss58_address}[/blue] to coldkey " + f"[blue]{destination_coldkey_ss58}[/blue]\n" + f"Amount: [green]{amount}[/green] from netuid [yellow]{origin_netuid}[/yellow] to netuid " + f"[yellow]{destination_netuid}[/yellow]" + ) + call = await subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="transfer_stake", + call_params={ + "destination_coldkey": destination_coldkey_ss58, + "hotkey": hotkey_ss58, + "origin_netuid": origin_netuid, + "destination_netuid": destination_netuid, + "alpha_amount": amount.rao, + }, + ) + + success, message = 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, + ) + + if success: + if not wait_for_finalization and not wait_for_inclusion: + return True + + logging.success(":white_heavy_check_mark: [green]Finalized[/green]") + + # Get updated stakes + origin_stake, dest_stake = await _get_stake_in_origin_and_dest( + subtensor=subtensor, + origin_hotkey_ss58=hotkey_ss58, + destination_hotkey_ss58=hotkey_ss58, + origin_coldkey_ss58=wallet.coldkeypub.ss58_address, + destination_coldkey_ss58=destination_coldkey_ss58, + origin_netuid=origin_netuid, + destination_netuid=destination_netuid, + ) + logging.info( + f"Origin Stake: [blue]{stake_in_origin}[/blue] :arrow_right: [green]{origin_stake}[/green]" + ) + logging.info( + f"Destination Stake: [blue]{stake_in_destination}[/blue] :arrow_right: [green]{dest_stake}[/green]" + ) + + return True + else: + logging.error(f":cross_mark: [red]Failed[/red]: {message}") + return False + + except Exception as e: + logging.error(f":cross_mark: [red]Failed[/red]: {str(e)}") + return False + + +async def swap_stake_extrinsic( + subtensor: "AsyncSubtensor", + wallet: "Wallet", + hotkey_ss58: str, + origin_netuid: int, + destination_netuid: int, + amount: Balance, + safe_swapping: bool = False, + allow_partial_stake: bool = False, + rate_tolerance: float = 0.005, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> bool: + """ + Swaps stake from one subnet to another for a given hotkey in the Bittensor network. + + Parameters: + subtensor: Subtensor instance. + wallet: The wallet to swap stake from. + hotkey_ss58: The hotkey SS58 address associated with the stake. + origin_netuid: The source subnet UID. + destination_netuid: The destination subnet UID. + amount: Amount to swap. + safe_swapping: If true, enables price safety checks to protect against price impact. + allow_partial_stake: If true, allows partial stake swaps when the full amount would exceed the price tolerance. + rate_tolerance: Maximum allowed increase in a price ratio (0.005 = 0.5%). + 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. + + + Returns: + success (bool): True if the swap was successful. + """ + amount.set_unit(netuid=origin_netuid) + + # Check sufficient stake + stake_in_origin, stake_in_destination = await _get_stake_in_origin_and_dest( + subtensor=subtensor, + origin_hotkey_ss58=hotkey_ss58, + destination_hotkey_ss58=hotkey_ss58, + origin_coldkey_ss58=wallet.coldkeypub.ss58_address, + destination_coldkey_ss58=wallet.coldkeypub.ss58_address, + origin_netuid=origin_netuid, + destination_netuid=destination_netuid, + ) + if stake_in_origin < amount: + logging.error( + f":cross_mark: [red]Failed[/red]: Insufficient stake in origin hotkey: {hotkey_ss58}. " + f"Stake: {stake_in_origin}, amount: {amount}" + ) + return False + + try: + call_params = { + "hotkey": hotkey_ss58, + "origin_netuid": origin_netuid, + "destination_netuid": destination_netuid, + "alpha_amount": amount.rao, + } + + if safe_swapping: + origin_pool, destination_pool = await asyncio.gather( + subtensor.subnet(netuid=origin_netuid), + subtensor.subnet(netuid=destination_netuid), + ) + swap_rate_ratio = origin_pool.price.rao / destination_pool.price.rao + swap_rate_ratio_with_tolerance = swap_rate_ratio * (1 + rate_tolerance) + + logging.info( + f"Swapping stake with safety for hotkey [blue]{hotkey_ss58}[/blue]\n" + f"Amount: [green]{amount}[/green] from netuid [green]{origin_netuid}[/green] to netuid " + f"[green]{destination_netuid}[/green]\n" + f"Current price ratio: [green]{swap_rate_ratio:.4f}[/green], " + f"Ratio with tolerance: [green]{swap_rate_ratio_with_tolerance:.4f}[/green]" + ) + call_params.update( + { + "limit_price": swap_rate_ratio_with_tolerance, + "allow_partial": allow_partial_stake, + } + ) + call_function = "swap_stake_limit" + else: + logging.info( + f"Swapping stake for hotkey [blue]{hotkey_ss58}[/blue]\n" + f"Amount: [green]{amount}[/green] from netuid [green]{origin_netuid}[/green] to netuid " + f"[green]{destination_netuid}[/green]" + ) + call_function = "swap_stake" + + call = await subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function=call_function, + call_params=call_params, + ) + + success, err_msg = 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, + ) + + if success: + if not wait_for_finalization and not wait_for_inclusion: + return True + + logging.success(":white_heavy_check_mark: [green]Finalized[/green]") + + # Get updated stakes + origin_stake, dest_stake = await _get_stake_in_origin_and_dest( + subtensor, + origin_hotkey_ss58=hotkey_ss58, + destination_hotkey_ss58=hotkey_ss58, + origin_coldkey_ss58=wallet.coldkeypub.ss58_address, + destination_coldkey_ss58=wallet.coldkeypub.ss58_address, + origin_netuid=origin_netuid, + destination_netuid=destination_netuid, + ) + logging.info( + f"Origin Stake: [blue]{stake_in_origin}[/blue] :arrow_right: [green]{origin_stake}[/green]" + ) + logging.info( + f"Destination Stake: [blue]{stake_in_destination}[/blue] :arrow_right: [green]{dest_stake}[/green]" + ) + + return True + else: + if safe_swapping and "Custom error: 8" in err_msg: + logging.error( + ":cross_mark: [red]Failed[/red]: Price ratio exceeded tolerance limit. Either increase price tolerance or enable partial staking." + ) + else: + logging.error(f":cross_mark: [red]Failed[/red]: {err_msg}") + return False + + except Exception as e: + logging.error(f":cross_mark: [red]Failed[/red]: {str(e)}") + return False + + +async def move_stake_extrinsic( + subtensor: "AsyncSubtensor", + wallet: "Wallet", + origin_hotkey_ss58: str, + origin_netuid: int, + destination_hotkey_ss58: str, + destination_netuid: int, + amount: Optional[Balance] = None, + move_all_stake: bool = False, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> bool: + """ + Moves stake from one hotkey to another within subnets in the Bittensor network. + + Parameters: + subtensor: Subtensor instance. + wallet: The wallet to move stake from. + origin_hotkey_ss58: The SS58 address of the source hotkey. + origin_netuid: The netuid of the source subnet. + destination_hotkey_ss58: The SS58 address of the destination hotkey. + destination_netuid: The netuid of the destination subnet. + amount: Amount to move. + move_all_stake: If true, moves all stake from the source hotkey to the destination hotkey. + 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. + + Returns: + success: True if the move was successful. Otherwise, False. + """ + if not amount and not move_all_stake: + logging.error( + ":cross_mark: [red]Failed[/red]: Please specify an `amount` or `move_all_stake` argument to move stake." + ) + return False + + # Check sufficient stake + stake_in_origin, stake_in_destination = await _get_stake_in_origin_and_dest( + subtensor=subtensor, + origin_hotkey_ss58=origin_hotkey_ss58, + destination_hotkey_ss58=destination_hotkey_ss58, + origin_netuid=origin_netuid, + destination_netuid=destination_netuid, + origin_coldkey_ss58=wallet.coldkeypub.ss58_address, + destination_coldkey_ss58=wallet.coldkeypub.ss58_address, + ) + if move_all_stake: + amount = stake_in_origin + + elif stake_in_origin < amount: + logging.error( + f":cross_mark: [red]Failed[/red]: Insufficient stake in origin hotkey: {origin_hotkey_ss58}. " + f"Stake: {stake_in_origin}, amount: {amount}" + ) + return False + + amount.set_unit(netuid=origin_netuid) + + try: + logging.info( + f"Moving stake from hotkey [blue]{origin_hotkey_ss58}[/blue] to hotkey [blue]{destination_hotkey_ss58}[/blue]\n" + f"Amount: [green]{amount}[/green] from netuid [yellow]{origin_netuid}[/yellow] to netuid " + f"[yellow]{destination_netuid}[/yellow]" + ) + call = await subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="move_stake", + call_params={ + "origin_hotkey": origin_hotkey_ss58, + "origin_netuid": origin_netuid, + "destination_hotkey": destination_hotkey_ss58, + "destination_netuid": destination_netuid, + "alpha_amount": amount.rao, + }, + ) + + success, message = 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, + ) + + if success: + if not wait_for_finalization and not wait_for_inclusion: + return True + + logging.success(":white_heavy_check_mark: [green]Finalized[/green]") + + # Get updated stakes + origin_stake, dest_stake = await _get_stake_in_origin_and_dest( + subtensor=subtensor, + origin_hotkey_ss58=origin_hotkey_ss58, + destination_hotkey_ss58=destination_hotkey_ss58, + origin_coldkey_ss58=wallet.coldkeypub.ss58_address, + destination_coldkey_ss58=wallet.coldkeypub.ss58_address, + origin_netuid=origin_netuid, + destination_netuid=destination_netuid, + ) + logging.info( + f"Origin Stake: [blue]{stake_in_origin}[/blue] :arrow_right: [green]{origin_stake}[/green]" + ) + logging.info( + f"Destination Stake: [blue]{stake_in_destination}[/blue] :arrow_right: [green]{dest_stake}[/green]" + ) + + return True + else: + logging.error(f":cross_mark: [red]Failed[/red]: {message}") + return False + + except Exception as e: + logging.error(f":cross_mark: [red]Failed[/red]: {str(e)}") + return False diff --git a/bittensor/core/extrinsics/asyncex/registration.py b/bittensor/core/extrinsics/asyncex/registration.py new file mode 100644 index 0000000000..2e67368be5 --- /dev/null +++ b/bittensor/core/extrinsics/asyncex/registration.py @@ -0,0 +1,485 @@ +""" +This module provides async functionalities for registering a wallet with the subtensor network using Proof-of-Work (PoW). + +Extrinsics: +- register_extrinsic: Registers the wallet to the subnet. +- burned_register_extrinsic: Registers the wallet to chain by recycling TAO. +""" + +import asyncio +from typing import Optional, Union, TYPE_CHECKING + +from bittensor.core.extrinsics.asyncex.utils import get_extrinsic_fee +from bittensor.utils import unlock_key +from bittensor.utils.btlogging import logging +from bittensor.utils.registration import log_no_torch_error, create_pow_async, torch + +if TYPE_CHECKING: + from bittensor_wallet import Wallet + from bittensor.core.async_subtensor import AsyncSubtensor + + +async def burned_register_extrinsic( + subtensor: "AsyncSubtensor", + wallet: "Wallet", + netuid: int, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> bool: + """Registers the wallet to chain by recycling TAO. + + Parameters: + subtensor: Subtensor instance. + wallet: Bittensor wallet object. + netuid: The ``netuid`` of the subnet to register on. + 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. + + Returns: + success: True if the extrinsic was successful. Otherwise, False. + """ + block_hash = await subtensor.substrate.get_chain_head() + if not await subtensor.subnet_exists(netuid, block_hash=block_hash): + logging.error( + f":cross_mark: [red]Failed error:[/red] subnet [blue]{netuid}[/blue] does not exist." + ) + return False + + if not (unlock := unlock_key(wallet)).success: + logging.error(unlock.message) + return False + + logging.info( + f":satellite: [magenta]Checking Account on subnet[/magenta] [blue]{netuid}[/blue][magenta] ...[/magenta]" + ) + + # We could do this as_completed because we don't need old_balance and recycle + # if neuron is null, but the complexity isn't worth it considering the small performance + # gains we'd hypothetically receive in this situation + neuron, old_balance, recycle_amount = await asyncio.gather( + subtensor.get_neuron_for_pubkey_and_subnet( + wallet.hotkey.ss58_address, netuid=netuid, block_hash=block_hash + ), + subtensor.get_balance(wallet.coldkeypub.ss58_address, block_hash=block_hash), + subtensor.recycle(netuid=netuid, block_hash=block_hash), + ) + + if not neuron.is_null: + logging.info(":white_heavy_check_mark: [green]Already Registered[/green]") + logging.info(f"\t\tuid: [blue]{neuron.uid}[/blue]") + logging.info(f"\t\tnetuid: [blue]{neuron.netuid}[/blue]") + logging.info(f"\t\thotkey: [blue]{neuron.hotkey}[/blue]") + logging.info(f"\t\tcoldkey: [blue]{neuron.coldkey}[/blue]") + return True + + logging.debug(":satellite: [magenta]Recycling TAO for Registration...[/magenta]") + + # create extrinsic call + call = await subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="burned_register", + call_params={ + "netuid": netuid, + "hotkey": wallet.hotkey.ss58_address, + }, + ) + fee = await get_extrinsic_fee( + subtensor=subtensor, call=call, keypair=wallet.coldkeypub + ) + logging.info( + f"The registration fee for SN #[blue]{netuid}[/blue] is [blue]{fee}[/blue]." + ) + success, message = 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, + ) + + if not success: + logging.error(f":cross_mark: [red]Failed error:[/red] {message}") + await asyncio.sleep(0.5) + return False + + # TODO: It is worth deleting everything below and simply returning the result without additional verification. This + # should be the responsibility of the user. We will also reduce the number of calls to the chain. + # Successful registration, final check for neuron and pubkey + logging.info(":satellite: [magenta]Checking Balance...[/magenta]") + block_hash = await subtensor.substrate.get_chain_head() + new_balance = await subtensor.get_balance( + wallet.coldkeypub.ss58_address, block_hash=block_hash + ) + + logging.info( + 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 + ) + if is_registered: + logging.info(":white_heavy_check_mark: [green]Registered[/green]") + return True + + # neuron not found, try again + logging.error(":cross_mark: [red]Unknown error. Neuron not found.[/red]") + return False + + +async def register_subnet_extrinsic( + subtensor: "AsyncSubtensor", + wallet: "Wallet", + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> bool: + """ + Registers a new subnetwork on the Bittensor blockchain asynchronously. + + Parameters: + subtensor: The subtensor interface to send the extrinsic. + wallet: The wallet to be used for subnet registration. + 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. + + Returns: + bool: True if the subnet registration was successful, False otherwise. + """ + balance = await subtensor.get_balance(wallet.coldkeypub.ss58_address) + burn_cost = await subtensor.get_subnet_burn_cost() + + if burn_cost > balance: + logging.error( + f"Insufficient balance {balance} to register subnet. Current burn cost is {burn_cost} TAO" + ) + return False + + call = await subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="register_network", + call_params={ + "hotkey": wallet.hotkey.ss58_address, + "mechid": 1, + }, + ) + + success, message = 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, + ) + + if not wait_for_finalization and not wait_for_inclusion: + return True + + if success: + logging.success( + ":white_heavy_check_mark: [green]Successfully registered subnet[/green]" + ) + return True + + logging.error(f"Failed to register subnet: {message}") + return False + + +async def register_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, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> bool: + """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. + + 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. + 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. + + Returns: + bool: True if the subnet registration was successful, False otherwise. + """ + block_hash = await subtensor.substrate.get_chain_head() + logging.debug("[magenta]Checking subnet status... [/magenta]") + if not await subtensor.subnet_exists(netuid, block_hash=block_hash): + logging.error( + f":cross_mark: [red]Failed error:[/red] subnet [blue]{netuid}[/blue] does not exist." + ) + return False + + logging.info( + f":satellite: [magenta]Checking Account on subnet[/magenta] [blue]{netuid}[/blue] [magenta]...[/magenta]" + ) + neuron = await subtensor.get_neuron_for_pubkey_and_subnet( + hotkey_ss58=wallet.hotkey.ss58_address, netuid=netuid, block_hash=block_hash + ) + + if not neuron.is_null: + logging.info(":white_heavy_check_mark: [green]Already Registered[/green]") + logging.info(f"\t\tuid: [blue]{neuron.uid}[/blue]") + logging.info(f"\t\tnetuid: [blue]{neuron.netuid}[/blue]") + logging.info(f"\t\thotkey: [blue]{neuron.hotkey}[/blue]") + logging.info(f"\t\tcoldkey: [blue]{neuron.coldkey}[/blue]") + return True + + logging.debug( + f"Registration hotkey: {wallet.hotkey.ss58_address}, Public coldkey: " + f"{wallet.coldkey.ss58_address} in the network: {subtensor.network}." + ) + + if not torch: + log_no_torch_error() + return False + + # Attempt rolling registration. + attempts = 1 + + while True: + logging.info( + f":satellite: [magenta]Registering...[/magenta] [blue]({attempts}/{max_allowed_attempts})[/blue]" + ) + # Solve latest POW. + if cuda: + if not torch.cuda.is_available(): + return False + + 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: + 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: + logging.error( + f":white_heavy_check_mark: [green]Already registered on netuid:[/green] [blue]{netuid}[/blue]" + ) + return True + + # pow successful, proceed to submit pow to chain for registration + else: + logging.info(":satellite: [magenta]Submitting POW...[/magenta]") + # check if a pow result is still valid + while not await pow_result.is_stale_async(subtensor=subtensor): + call = await subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="register", + call_params={ + "netuid": netuid, + "block_number": pow_result.block_number, + "nonce": pow_result.nonce, + "work": [int(byte_) for byte_ in pow_result.seal], + "hotkey": wallet.hotkey.ss58_address, + "coldkey": wallet.coldkeypub.ss58_address, + }, + ) + success, message = 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, + ) + + if not success: + # Look error here + # https://github.com/opentensor/subtensor/blob/development/pallets/subtensor/src/errors.rs + + if "HotKeyAlreadyRegisteredInSubNet" in message: + logging.info( + f":white_heavy_check_mark: [green]Already Registered on subnet:[/green] " + f"[blue]{netuid}[/blue]." + ) + return True + logging.error(f":cross_mark: [red]Failed[/red]: {message}") + await asyncio.sleep(0.5) + + # Successful registration, final check for neuron and pubkey + if success: + logging.info(":satellite: Checking Registration status...") + is_registered = await subtensor.is_hotkey_registered( + netuid=netuid, hotkey_ss58=wallet.hotkey.ss58_address + ) + if is_registered: + logging.success( + ":white_heavy_check_mark: [green]Registered[/green]" + ) + return True + else: + # neuron not found, try again + logging.error( + ":cross_mark: [red]Unknown error. Neuron not found.[/red]" + ) + continue + else: + # Exited loop because pow is no longer valid. + logging.error("[red]POW is stale.[/red]") + # Try again. + + if attempts < max_allowed_attempts: + # Failed registration, retry pow + attempts += 1 + logging.error( + f":satellite: [magenta]Failed registration, retrying pow ...[/magenta] " + f"[blue]({attempts}/{max_allowed_attempts})[/blue]" + ) + else: + # Failed to register after max attempts. + logging.error("[red]No more attempts.[/red]") + return False + + +async def set_subnet_identity_extrinsic( + subtensor: "AsyncSubtensor", + wallet: "Wallet", + netuid: int, + subnet_name: str, + github_repo: str, + subnet_contact: str, + subnet_url: str, + logo_url: str, + discord: str, + description: str, + additional: str, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> tuple[bool, str]: + """ + Set the identity information for a given subnet. + + Parameters: + subtensor: An instance of the Subtensor class to interact with the blockchain. + wallet: A wallet instance used to sign and submit the extrinsic. + netuid: The unique ID for the subnet. + subnet_name: The name of the subnet to assign the identity information. + github_repo: URL of the GitHub repository related to the subnet. + subnet_contact: Subnet's contact information, e.g., email or contact link. + subnet_url: The URL of the subnet's primary web portal. + logo_url: The URL of the logo's primary web portal. + discord: Discord server or contact for the subnet. + description: A textual description of the subnet. + additional: Any additional metadata or information related to the subnet. + 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. + + Returns: + Tuple[bool, str]: + - True and a success message if the extrinsic is successfully submitted or processed. + - False and an error message if the submission fails or the wallet cannot be unlocked. + """ + + if not (unlock := unlock_key(wallet)).success: + logging.error(unlock.message) + return False, unlock.message + + call = await subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="set_subnet_identity", + call_params={ + "hotkey": wallet.hotkey.ss58_address, + "netuid": netuid, + "subnet_name": subnet_name, + "github_repo": github_repo, + "subnet_contact": subnet_contact, + "subnet_url": subnet_url, + "logo_url": logo_url, + "discord": discord, + "description": description, + "additional": additional, + }, + ) + + success, message = 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, + ) + + if not wait_for_finalization and not wait_for_inclusion: + return True, message + + if success: + logging.success( + f":white_heavy_check_mark: [green]Identities for subnet[/green] [blue]{netuid}[/blue] [green]are set.[/green]" + ) + return True, f"Identities for subnet {netuid} are set." + + logging.error( + f":cross_mark: Failed to set identity for subnet [blue]{netuid}[/blue]: {message}" + ) + return False, f"Failed to set identity for subnet {netuid}: {message}" diff --git a/bittensor/core/extrinsics/asyncex/root.py b/bittensor/core/extrinsics/asyncex/root.py new file mode 100644 index 0000000000..bb318cdb90 --- /dev/null +++ b/bittensor/core/extrinsics/asyncex/root.py @@ -0,0 +1,140 @@ +import asyncio +from typing import Optional, TYPE_CHECKING + +from bittensor.utils import u16_normalized_float, unlock_key +from bittensor.utils.balance import Balance +from bittensor.utils.btlogging import logging + +if TYPE_CHECKING: + from bittensor_wallet import Wallet + from bittensor.core.async_subtensor import AsyncSubtensor + + +async def _get_limits(subtensor: "AsyncSubtensor") -> tuple[int, float]: + """ + Retrieves the minimum allowed weights and maximum weight limit for the given subnet. + + These values are fetched asynchronously using `asyncio.gather` to run both requests concurrently. + + Args: + subtensor (AsyncSubtensor): The AsyncSubtensor object used to interface with the network's substrate node. + + Returns: + tuple[int, float]: A tuple containing: + - `min_allowed_weights` (int): The minimum allowed weights. + - `max_weight_limit` (float): The maximum weight limit, normalized to a float value. + """ + # Get weight restrictions. + maw, mwl = await asyncio.gather( + subtensor.get_hyperparameter("MinAllowedWeights", netuid=0), + subtensor.get_hyperparameter("MaxWeightsLimit", netuid=0), + ) + min_allowed_weights = int(maw) + max_weight_limit = u16_normalized_float(int(mwl)) + return min_allowed_weights, max_weight_limit + + +async def root_register_extrinsic( + subtensor: "AsyncSubtensor", + wallet: "Wallet", + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> bool: + """ + Registers the neuron to the root network. + + Arguments: + subtensor: Subtensor instance to interact with the blockchain. + wallet: Bittensor Wallet instance. + 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. + + Returns: + bool: True if the subnet registration was successful, False otherwise. + """ + netuid = 0 + logging.info( + f"Registering on netuid [blue]{netuid}[/blue] on network: [blue]{subtensor.network}[/blue]" + ) + + logging.info("Fetching recycle amount & balance.") + block_hash = await subtensor.get_block_hash() + recycle_call, balance = await asyncio.gather( + subtensor.get_hyperparameter( + param_name="Burn", + netuid=netuid, + block_hash=block_hash, + ), + subtensor.get_balance( + wallet.coldkeypub.ss58_address, + block_hash=block_hash, + ), + ) + + current_recycle = Balance.from_rao(int(recycle_call)) + + if balance < current_recycle: + logging.error( + f"[red]Insufficient balance {balance} to register neuron. " + f"Current recycle is {current_recycle} TAO[/red]." + ) + return False + + if not (unlock := unlock_key(wallet)).success: + logging.error(unlock.message) + return False + + logging.debug( + f"Checking if hotkey ([blue]{wallet.hotkey_str}[/blue]) is registered on root." + ) + is_registered = await subtensor.is_hotkey_registered( + netuid=netuid, hotkey_ss58=wallet.hotkey.ss58_address + ) + if is_registered: + logging.error( + ":white_heavy_check_mark: [green]Already registered on root network.[/green]" + ) + return True + + logging.info(":satellite: [magenta]Registering to root network...[/magenta]") + call = await subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="root_register", + call_params={"hotkey": wallet.hotkey.ss58_address}, + ) + success, message = 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, + ) + + if not success: + logging.error(f":cross_mark: [red]Failed error:[/red] {message}") + await asyncio.sleep(0.5) + return False + + # Successful registration, final check for neuron and pubkey + else: + uid = await subtensor.substrate.query( + module="SubtensorModule", + storage_function="Uids", + params=[netuid, wallet.hotkey.ss58_address], + ) + if uid is not None: + logging.info( + f":white_heavy_check_mark: [green]Registered with UID[/green] [blue]{uid}[/blue]." + ) + return True + else: + # neuron not found, try again + logging.error(":cross_mark: [red]Unknown error. Neuron not found.[/red]") + return False diff --git a/bittensor/core/extrinsics/asyncex/serving.py b/bittensor/core/extrinsics/asyncex/serving.py new file mode 100644 index 0000000000..6a511718d1 --- /dev/null +++ b/bittensor/core/extrinsics/asyncex/serving.py @@ -0,0 +1,367 @@ +import asyncio +from typing import Optional, Union, TYPE_CHECKING + +from bittensor.core.errors import MetadataError +from bittensor.core.settings import version_as_int +from bittensor.core.types import AxonServeCallParams +from bittensor.utils import ( + networking as net, + unlock_key, + Certificate, +) +from bittensor.utils.btlogging import logging + +if TYPE_CHECKING: + from bittensor.core.axon import Axon + from bittensor.core.async_subtensor import AsyncSubtensor + from bittensor_wallet import Wallet + + +async def do_serve_axon( + subtensor: "AsyncSubtensor", + wallet: "Wallet", + call_params: "AxonServeCallParams", + wait_for_inclusion: bool = False, + wait_for_finalization: bool = True, + period: Optional[int] = None, +) -> tuple[bool, str]: + """ + Internal method to submit a serve axon transaction to the Bittensor blockchain. This method creates and submits a + transaction, enabling a neuron's ``Axon`` to serve requests on the network. + + Args: + subtensor (bittensor.core.async_subtensor.AsyncSubtensor): Subtensor instance object. + wallet (bittensor_wallet.Wallet): The wallet associated with the neuron. + call_params (bittensor.core.types.AxonServeCallParams): Parameters required for the serve axon call. + wait_for_inclusion (bool): Waits for the transaction to be included in a block. + wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. + period (Optional[int]): 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. + + Returns: + tuple[bool, str]: A tuple containing a success flag and an optional error message. + + This function is crucial for initializing and announcing a neuron's ``Axon`` service on the network, enhancing the + decentralized computation capabilities of Bittensor. + """ + + if call_params.certificate is None: + call_function = "serve_axon" + else: + call_function = "serve_axon_tls" + + call = await subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function=call_function, + call_params=call_params.dict(), + ) + success, message = await subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + sign_with="hotkey", + period=period, + ) + return success, message + + +async def serve_extrinsic( + subtensor: "AsyncSubtensor", + wallet: "Wallet", + ip: str, + port: int, + protocol: int, + netuid: int, + placeholder1: int = 0, + placeholder2: int = 0, + certificate: Optional[Certificate] = None, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> bool: + """ + Subscribes a Bittensor endpoint to the subtensor chain. + + Parameters: + subtensor: Subtensor instance object. + wallet: Bittensor wallet object. + ip: Endpoint host port i.e., ``192.122.31.4``. + port: Endpoint port number i.e., ``9221``. + protocol: An ``int`` representation of the protocol. + netuid: The network uid to serve on. + placeholder1: A placeholder for future use. + placeholder2: A placeholder for future use. + certificate: Certificate to use for TLS. If ``None``, no TLS will be used. + 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. + + Returns: + bool: True if the subnet registration was successful, False otherwise. + """ + # Decrypt hotkey + if not (unlock := unlock_key(wallet, "hotkey")).success: + logging.error(unlock.message) + return False + + params = AxonServeCallParams( + **{ + "version": version_as_int, + "ip": net.ip_to_int(ip), + "port": port, + "ip_type": net.ip_version(ip), + "netuid": netuid, + "hotkey": wallet.hotkey.ss58_address, + "coldkey": wallet.coldkeypub.ss58_address, + "protocol": protocol, + "placeholder1": placeholder1, + "placeholder2": placeholder2, + "certificate": certificate, + } + ) + logging.debug("Checking axon ...") + neuron = await subtensor.get_neuron_for_pubkey_and_subnet( + wallet.hotkey.ss58_address, netuid=netuid + ) + neuron_up_to_date = not neuron.is_null and params == neuron + if neuron_up_to_date: + logging.debug( + f"Axon already served on: [blue]AxonInfo({wallet.hotkey.ss58_address}, {ip}:{port})[/blue]" + ) + return True + + logging.debug( + f"Serving axon with: [blue]AxonInfo({wallet.hotkey.ss58_address}, {ip}:{port})[/blue] -> " + f"[green]{subtensor.network}:{netuid}[/green]" + ) + + if params.certificate is None: + call_function = "serve_axon" + else: + call_function = "serve_axon_tls" + + call = await subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function=call_function, + call_params=params.dict(), + ) + success, message = await subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + sign_with="hotkey", + period=period, + raise_error=raise_error, + ) + + if success: + logging.debug( + f"Axon served with: [blue]AxonInfo({wallet.hotkey.ss58_address}, {ip}:{port})[/blue] on " + f"[green]{subtensor.network}:{netuid}[/green]" + ) + return True + + logging.error(f"Failed: {message}") + return False + + +async def serve_axon_extrinsic( + subtensor: "AsyncSubtensor", + netuid: int, + axon: "Axon", + certificate: Optional[Certificate] = None, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> bool: + """ + Serves the axon to the network. + + Parameters: + subtensor: AsyncSubtensor instance object. + netuid (int): The ``netuid`` being served on. + axon (bittensor.core.axon.Axon): Axon to serve. + certificate (bittensor.utils.Certificate): Certificate to use for TLS. If ``None``, no TLS will be used. + Defaults to ``None``. + 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. + + Returns: + bool: True if the subnet registration was successful, False otherwise. + """ + if not (unlock := unlock_key(axon.wallet, "hotkey")).success: + logging.error(unlock.message) + return False + external_port = axon.external_port + + # ---- Get external ip ---- + if axon.external_ip is None: + try: + external_ip = await asyncio.get_running_loop().run_in_executor( + None, net.get_external_ip + ) + logging.success( + f":white_heavy_check_mark: [green]Found external ip:[/green] [blue]{external_ip}[/blue]" + ) + except Exception as e: + raise ConnectionError( + f"Unable to attain your external ip. Check your internet connection. error: {e}" + ) from e + else: + external_ip = axon.external_ip + + # ---- Subscribe to chain ---- + serve_success = await serve_extrinsic( + subtensor=subtensor, + wallet=axon.wallet, + ip=external_ip, + port=external_port, + protocol=4, + netuid=netuid, + certificate=certificate, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + return serve_success + + +async def publish_metadata( + subtensor: "AsyncSubtensor", + wallet: "Wallet", + netuid: int, + data_type: str, + data: Union[bytes, dict], + period: Optional[int] = None, + reset_bonds: bool = False, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> bool: + """ + Publishes metadata on the Bittensor network using the specified wallet and network identifier. + + Parameters: + subtensor: The subtensor instance representing the Bittensor blockchain connection. + wallet: The wallet object used for authentication in the transaction. + netuid: Network UID on which the metadata is to be published. + data_type: The data type of the information being submitted. It should be one of the following: + ``'Sha256'``, ``'Blake256'``, ``'Keccak256'``, or ``'Raw0-128'``. This specifies the format or hashing + algorithm used for the data. + data: The actual metadata content to be published. This should be formatted or hashed + according to the ``type`` specified. (Note: max ``str`` length is 128 bytes for ``'Raw0-128'``.) + reset_bonds: If `True`, the function will reset the bonds for the neuron. Defaults to `False`. + 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. + + Returns: + bool: True if the subnet registration was successful, False otherwise. + + Raises: + MetadataError: If there is an error in submitting the extrinsic, or if the response from the blockchain indicates + failure. + """ + + if not (unlock := unlock_key(wallet, "hotkey")).success: + logging.error(unlock.message) + return False + + fields = [{f"{data_type}": data}] + if reset_bonds: + fields.append({"ResetBondsFlag": b""}) + + async with subtensor.substrate as substrate: + call = await substrate.compose_call( + call_module="Commitments", + call_function="set_commitment", + call_params={ + "netuid": netuid, + "info": {"fields": [fields]}, + }, + ) + + success, message = await subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + sign_with="hotkey", + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + period=period, + raise_error=raise_error, + ) + + if success: + return True + raise MetadataError(message) + + +async def get_metadata( + subtensor: "AsyncSubtensor", + netuid: int, + hotkey: str, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, +) -> Union[str, dict]: + """Fetches metadata from the blockchain for a given hotkey and netuid.""" + async with subtensor.substrate: + block_hash = await subtensor.determine_block_hash( + block, block_hash, reuse_block + ) + commit_data = await subtensor.substrate.query( + module="Commitments", + storage_function="CommitmentOf", + params=[netuid, hotkey], + block_hash=block_hash, + reuse_block_hash=reuse_block, + ) + return commit_data + + +async def get_last_bonds_reset( + subtensor: "AsyncSubtensor", + netuid: int, + hotkey: str, + block: Optional[int] = None, + block_hash: Optional[str] = None, + reuse_block: bool = False, +) -> bytes: + """ + Fetches the last bonds reset triggered at commitment from the blockchain for a given hotkey and netuid. + + Parameters: + subtensor: Subtensor instance object. + netuid: The network uid to fetch from. + hotkey: The hotkey of the neuron for which to fetch the last bonds reset. + block: The block number to query. If ``None``, the latest block is used. + block_hash: The hash of the block to retrieve the parameter from. Do not specify if using block or reuse_block. + reuse_block: Whether to use the last-used block. Do not set if using block_hash or block. + + Returns: + bytes: The last bonds reset data for the specified hotkey and netuid. + """ + block_hash = await subtensor.determine_block_hash(block, block_hash, reuse_block) + block = await subtensor.substrate.query( + module="Commitments", + storage_function="LastBondsReset", + params=[netuid, hotkey], + block_hash=block_hash, + ) + return block diff --git a/bittensor/core/extrinsics/asyncex/staking.py b/bittensor/core/extrinsics/asyncex/staking.py new file mode 100644 index 0000000000..c06c58cd78 --- /dev/null +++ b/bittensor/core/extrinsics/asyncex/staking.py @@ -0,0 +1,379 @@ +import asyncio +from typing import Optional, Sequence, TYPE_CHECKING + +from async_substrate_interface.errors import SubstrateRequestException + +from bittensor.core.extrinsics.utils import get_old_stakes +from bittensor.utils import unlock_key, format_error_message +from bittensor.utils.balance import Balance +from bittensor.utils.btlogging import logging + +if TYPE_CHECKING: + from bittensor_wallet import Wallet + from bittensor.core.async_subtensor import AsyncSubtensor + + +async def add_stake_extrinsic( + subtensor: "AsyncSubtensor", + wallet: "Wallet", + netuid: int, + hotkey_ss58: str, + amount: Balance, + safe_staking: bool = False, + allow_partial_stake: bool = False, + rate_tolerance: float = 0.005, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> bool: + """ + Adds a stake from the specified wallet to the neuron identified by the SS58 address of its hotkey in specified subnet. + Staking is a fundamental process in the Bittensor network that enables neurons to participate actively and earn + incentives. + + Parameters: + subtensor: Subtensor instance with the connection to the chain. + wallet: Bittensor wallet object. + netuid: The unique identifier of the subnet to which the neuron belongs. + hotkey_ss58: The `ss58` address of the hotkey account to stake to default to the wallet's hotkey. + amount: Amount to stake as Bittensor balance in TAO always. + safe_staking: If True, enables price safety checks. Default is ``False``. + allow_partial_stake: If True, allows partial unstaking if price tolerance exceeded. Default is ``False``. + rate_tolerance: Maximum allowed price increase percentage (0.005 = 0.5%). Default is ``0.005``. + 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. + + Returns: + bool: True if the subnet registration was successful, False otherwise. + + Raises: + SubstrateRequestException: Raised if the extrinsic fails to be included in the block within the timeout. + """ + + # Decrypt keys, + if not (unlock := unlock_key(wallet)).success: + logging.error(unlock.message) + return False + + logging.info( + f":satellite: [magenta]Syncing with chain:[/magenta] [blue]{subtensor.network}[/blue] [magenta]...[/magenta]" + ) + old_balance = await subtensor.get_balance(wallet.coldkeypub.ss58_address) + block_hash = await subtensor.substrate.get_chain_head() + + # Get current stake and existential deposit + old_stake, existential_deposit = await asyncio.gather( + subtensor.get_stake( + coldkey_ss58=wallet.coldkeypub.ss58_address, + hotkey_ss58=hotkey_ss58, + netuid=netuid, + block_hash=block_hash, + ), + subtensor.get_existential_deposit(block_hash=block_hash), + ) + + # Leave existential balance to keep key alive. + if amount > old_balance - existential_deposit: + # If we are staking all, we need to leave at least the existential deposit. + amount = old_balance - existential_deposit + else: + amount = amount + + # Check enough to stake. + if amount > old_balance: + logging.error(":cross_mark: [red]Not enough stake:[/red]") + logging.error(f"\t\tbalance:{old_balance}") + logging.error(f"\t\tamount: {amount}") + logging.error(f"\t\twallet: {wallet.name}") + return False + + call_params = { + "hotkey": hotkey_ss58, + "netuid": netuid, + "amount_staked": amount.rao, + } + + if safe_staking: + pool = await subtensor.subnet(netuid=netuid) + base_price = pool.price.tao + + price_with_tolerance = ( + base_price if pool.netuid == 0 else base_price * (1 + rate_tolerance) + ) + + logging.info( + f":satellite: [magenta]Safe Staking to:[/magenta] " + f"[blue]netuid: [green]{netuid}[/green], amount: [green]{amount}[/green], " + f"tolerance percentage: [green]{rate_tolerance * 100}%[/green], " + f"price limit: [green]{price_with_tolerance}[/green], " + f"original price: [green]{base_price}[/green], " + f"with partial stake: [green]{allow_partial_stake}[/green] " + f"on [blue]{subtensor.network}[/blue][/magenta]...[/magenta]" + ) + + limit_price = Balance.from_tao(price_with_tolerance).rao + call_params.update( + { + "limit_price": limit_price, + "allow_partial": allow_partial_stake, + } + ) + call_function = "add_stake_limit" + else: + logging.info( + f":satellite: [magenta]Staking to:[/magenta] " + f"[blue]netuid: [green]{netuid}[/green], amount: [green]{amount}[/green] " + f"on [blue]{subtensor.network}[/blue][magenta]...[/magenta]" + ) + call_function = "add_stake" + + call = await subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function=call_function, + call_params=call_params, + ) + success, message = await subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + nonce_key="coldkeypub", + sign_with="coldkey", + use_nonce=True, + period=period, + raise_error=raise_error, + ) + if success: # If we successfully staked. + # We only wait here if we expect finalization. + if not wait_for_finalization and not wait_for_inclusion: + return True + + logging.success(":white_heavy_check_mark: [green]Finalized[/green]") + + logging.info( + f":satellite: [magenta]Checking Balance on:[/magenta] " + f"[blue]{subtensor.network}[/blue] [magenta]...[/magenta]" + ) + new_block_hash = await subtensor.substrate.get_chain_head() + new_balance, new_stake = await asyncio.gather( + subtensor.get_balance( + wallet.coldkeypub.ss58_address, block_hash=new_block_hash + ), + subtensor.get_stake( + coldkey_ss58=wallet.coldkeypub.ss58_address, + hotkey_ss58=hotkey_ss58, + netuid=netuid, + block_hash=new_block_hash, + ), + ) + + logging.info( + f"Balance: [blue]{old_balance}[/blue] :arrow_right: [green]{new_balance}[/green]" + ) + logging.info( + f"Stake: [blue]{old_stake}[/blue] :arrow_right: [green]{new_stake}[/green]" + ) + return True + + if safe_staking and "Custom error: 8" in message: + logging.error( + ":cross_mark: [red]Failed[/red]: Price exceeded tolerance limit. Either increase price tolerance or enable partial staking." + ) + else: + logging.error(f":cross_mark: [red]Failed: {message}.[/red]") + return False + + +async def add_stake_multiple_extrinsic( + subtensor: "AsyncSubtensor", + wallet: "Wallet", + hotkey_ss58s: list[str], + netuids: list[int], + amounts: list[Balance], + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> bool: + """ + Adds stake to each ``hotkey_ss58`` in the list, using each amount, from a common coldkey on subnet with + corresponding netuid. + + Parameters: + subtensor: Subtensor instance with the connection to the chain. + wallet: Bittensor wallet object for the coldkey. + netuids: List of netuids to stake to. + hotkey_ss58s: List of hotkeys to stake to. + amounts: List of corresponding TAO amounts to bet for each netuid and hotkey. + 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. + + Returns: + bool: True if the subnet registration was successful, False otherwise. + """ + # Decrypt keys, + if not (unlock := unlock_key(wallet)).success: + logging.error(unlock.message) + return False + + assert all( + [ + isinstance(netuids, list), + isinstance(hotkey_ss58s, list), + isinstance(amounts, list), + ] + ), "The `netuids`, `hotkey_ss58s` and `amounts` must be lists." + + if len(hotkey_ss58s) == 0: + return True + + assert len(netuids) == len(hotkey_ss58s) == len(amounts), ( + "The number of items in `netuids`, `hotkey_ss58s` and `amounts` must be the same." + ) + + if not all(isinstance(hotkey_ss58, str) for hotkey_ss58 in hotkey_ss58s): + raise TypeError("hotkey_ss58s must be a list of str") + + new_amounts: Sequence[Optional[Balance]] = [ + amount.set_unit(netuid) for amount, netuid in zip(amounts, netuids) + ] + + if sum(amount.tao for amount in new_amounts) == 0: + # Staking 0 tao + return True + + logging.info( + f":satellite: [magenta]Syncing with chain:[/magenta] [blue]{subtensor.network}[/blue] [magenta]...[/magenta]" + ) + block_hash = await subtensor.substrate.get_chain_head() + + all_stakes = await subtensor.get_stake_for_coldkey( + coldkey_ss58=wallet.coldkeypub.ss58_address, block_hash=block_hash + ) + old_stakes: list[Balance] = get_old_stakes( + wallet=wallet, hotkey_ss58s=hotkey_ss58s, netuids=netuids, all_stakes=all_stakes + ) + + # Remove existential balance to keep key alive. + # Keys must maintain a balance of at least 1000 rao to stay alive. + total_staking_rao = sum( + [amount.rao if amount is not None else 0 for amount in new_amounts] + ) + old_balance = initial_balance = await subtensor.get_balance( + wallet.coldkeypub.ss58_address, block_hash=block_hash + ) + + if total_staking_rao == 0: + # Staking all to the first wallet. + if old_balance.rao > 1000: + old_balance -= Balance.from_rao(1000) + + elif total_staking_rao < 1000: + # Staking less than 1000 rao to the wallets. + pass + else: + # Staking more than 1000 rao to the wallets. + # Reduce the amount to stake to each wallet to keep the balance above 1000 rao. + percent_reduction = 1 - (1000 / total_staking_rao) + new_amounts = [ + Balance.from_tao(amount.tao * percent_reduction) for amount in new_amounts + ] + + successful_stakes = 0 + for idx, (hotkey_ss58, amount, old_stake, netuid) in enumerate( + zip(hotkey_ss58s, new_amounts, old_stakes, netuids) + ): + # Check enough to stake + if amount > old_balance: + logging.error( + f":cross_mark: [red]Not enough balance[/red]: [green]{old_balance}[/green] to stake: " + f"[blue]{amount}[/blue] from wallet: [white]{wallet.name}[/white]" + ) + continue + + try: + logging.info( + f"Staking [blue]{amount}[/blue] to hotkey: [magenta]{hotkey_ss58}[/magenta] on netuid: " + f"[blue]{netuid}[/blue]" + ) + call = await subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="add_stake", + call_params={ + "hotkey": hotkey_ss58, + "amount_staked": amount.rao, + "netuid": netuid, + }, + ) + success, message = await subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + nonce_key="coldkeypub", + sign_with="coldkey", + use_nonce=True, + period=period, + raise_error=raise_error, + ) + + # If we successfully staked. + if success: + if not wait_for_finalization and not wait_for_inclusion: + old_balance -= amount + successful_stakes += 1 + continue + + logging.success(":white_heavy_check_mark: [green]Finalized[/green]") + + new_block_hash = await subtensor.substrate.get_chain_head() + new_stake, new_balance = await asyncio.gather( + subtensor.get_stake( + coldkey_ss58=wallet.coldkeypub.ss58_address, + hotkey_ss58=hotkey_ss58, + netuid=netuid, + block_hash=new_block_hash, + ), + subtensor.get_balance( + wallet.coldkeypub.ss58_address, block_hash=new_block_hash + ), + ) + logging.info( + f"Stake ({hotkey_ss58}) on netuid {netuid}: [blue]{old_stake}[/blue] :arrow_right: " + f"[green]{new_stake}[/green]" + ) + logging.info( + f"Balance: [blue]{old_balance}[/blue] :arrow_right: [green]{new_balance}[/green]" + ) + old_balance = new_balance + successful_stakes += 1 + else: + logging.error(f":cross_mark: [red]Failed: {message}.[/red]") + continue + + except SubstrateRequestException as error: + logging.error( + f":cross_mark: [red]Add Stake Multiple error: {format_error_message(error)}[/red]" + ) + + if successful_stakes != 0: + logging.info( + f":satellite: [magenta]Checking Balance on:[/magenta] [blue]{subtensor.network}[/blue] " + f"[magenta]...[/magenta]" + ) + new_balance = await subtensor.get_balance(wallet.coldkeypub.ss58_address) + logging.info( + f"Balance: [blue]{initial_balance}[/blue] :arrow_right: [green]{new_balance}[/green]" + ) + return True + + return False diff --git a/bittensor/core/extrinsics/asyncex/start_call.py b/bittensor/core/extrinsics/asyncex/start_call.py new file mode 100644 index 0000000000..3a2c64937a --- /dev/null +++ b/bittensor/core/extrinsics/asyncex/start_call.py @@ -0,0 +1,66 @@ +from typing import TYPE_CHECKING, Optional + +from bittensor.utils import unlock_key +from bittensor.utils.btlogging import logging + +if TYPE_CHECKING: + from bittensor_wallet import Wallet + from bittensor.core.async_subtensor import AsyncSubtensor + + +async def start_call_extrinsic( + subtensor: "AsyncSubtensor", + wallet: "Wallet", + netuid: int, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, +) -> tuple[bool, str]: + """ + Submits a start_call extrinsic to the blockchain, to trigger the start call process for a subnet (used to start a + new subnet's emission mechanism). + + Parameters: + subtensor: The Subtensor client instance used for blockchain interaction. + wallet: The wallet used to sign the extrinsic (must be unlocked). + netuid: The UID of the target subnet for which the call is being initiated. + 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 extrinsic to be included in a block. + wait_for_finalization: Whether to wait for finalization of the extrinsic. + + Returns: + Tuple[bool, str]: + - True and a success message if the extrinsic is successfully submitted or processed. + - False and an error message if the submission fails or the wallet cannot be unlocked. + """ + if not (unlock := unlock_key(wallet)).success: + logging.error(unlock.message) + return False, unlock.message + + async with subtensor.substrate as substrate: + start_call = await substrate.compose_call( + call_module="SubtensorModule", + call_function="start_call", + call_params={"netuid": netuid}, + ) + + success, message = await subtensor.sign_and_send_extrinsic( + call=start_call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + period=period, + raise_error=raise_error, + ) + + if not wait_for_finalization and not wait_for_inclusion: + return True, message + + if success: + return True, "Success with `start_call` response." + + return True, message diff --git a/bittensor/core/extrinsics/asyncex/take.py b/bittensor/core/extrinsics/asyncex/take.py new file mode 100644 index 0000000000..3840821c08 --- /dev/null +++ b/bittensor/core/extrinsics/asyncex/take.py @@ -0,0 +1,116 @@ +from typing import TYPE_CHECKING, Optional + +from bittensor_wallet.bittensor_wallet import Wallet + +from bittensor.utils import unlock_key + +if TYPE_CHECKING: + from bittensor.core.async_subtensor import AsyncSubtensor + + +async def increase_take_extrinsic( + subtensor: "AsyncSubtensor", + wallet: Wallet, + hotkey_ss58: str, + take: int, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> tuple[bool, str]: + """Sets the delegate 'take' percentage for a neuron identified by its hotkey. + + Parameters: + subtensor: The Subtensor instance. + wallet: The wallet to sign the extrinsic. + hotkey_ss58: SS58 address of the hotkey to set take for. + take: The percentage of rewards that the delegate claims from nominators. + 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. + + Returns: + Tuple[bool, str]: + - True and a success message if the extrinsic is successfully submitted or processed. + - False and an error message if the submission fails or the wallet cannot be unlocked. + """ + + unlock = unlock_key(wallet, raise_error=raise_error) + + if not unlock.success: + return False, unlock.message + + call = await subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="increase_take", + call_params={ + "hotkey": hotkey_ss58, + "take": take, + }, + ) + + return 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, + ) + + +async def decrease_take_extrinsic( + subtensor: "AsyncSubtensor", + wallet: Wallet, + hotkey_ss58: str, + take: int, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> tuple[bool, str]: + """ + Sets the delegate 'take' percentage for a neuron identified by its hotkey. + + Parameters: + subtensor: The Subtensor instance. + wallet: The wallet to sign the extrinsic. + hotkey_ss58: SS58 address of the hotkey to set take for. + take: The percentage of rewards that the delegate claims from nominators. + 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. + + Returns: + Tuple[bool, str]: + - True and a success message if the extrinsic is successfully submitted or processed. + - False and an error message if the submission fails or the wallet cannot be unlocked. + """ + unlock = unlock_key(wallet, raise_error=raise_error) + + if not unlock.success: + return False, unlock.message + + call = await subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="decrease_take", + call_params={ + "hotkey": hotkey_ss58, + "take": take, + }, + ) + + return 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, + ) diff --git a/bittensor/core/extrinsics/asyncex/transfer.py b/bittensor/core/extrinsics/asyncex/transfer.py new file mode 100644 index 0000000000..3e3d6d7193 --- /dev/null +++ b/bittensor/core/extrinsics/asyncex/transfer.py @@ -0,0 +1,144 @@ +import asyncio +from typing import TYPE_CHECKING, Optional + +from bittensor.core.settings import NETWORK_EXPLORER_MAP +from bittensor.utils import ( + get_explorer_url_for_network, + get_transfer_fn_params, + is_valid_bittensor_address_or_public_key, + unlock_key, +) +from bittensor.utils.balance import Balance +from bittensor.utils.btlogging import logging + +if TYPE_CHECKING: + from bittensor.core.async_subtensor import AsyncSubtensor + from bittensor_wallet import Wallet + + +async def transfer_extrinsic( + subtensor: "AsyncSubtensor", + wallet: "Wallet", + destination: str, + amount: Optional[Balance], + keep_alive: bool = True, + transfer_all: bool = False, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> bool: + """Transfers funds from this wallet to the destination public key address. + + Parameters: + subtensor: The Subtensor instance. + wallet: The wallet to sign the extrinsic. + destination: Destination public key address (ss58_address or ed25519) of recipient. + amount: Amount to stake as Bittensor balance. `None` if transferring all. + transfer_all: Whether to transfer all funds from this wallet to the destination address. + keep_alive: If set, keeps the account alive by keeping the balance above the existential deposit. + 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. + + Returns: + bool: True if the subnet registration was successful, False otherwise. + """ + if amount is None and not transfer_all: + logging.error("If not transferring all, `amount` must be specified.") + return False + + # Validate destination address. + if not is_valid_bittensor_address_or_public_key(destination): + logging.error( + f":cross_mark: [red]Invalid destination SS58 address[/red]: {destination}" + ) + return False + + logging.info(f"Initiating transfer on network: {subtensor.network}") + # Unlock wallet coldkey. + if not (unlock := unlock_key(wallet)).success: + logging.error(unlock.message) + return False + + # Check balance. + logging.info( + f":satellite: [magenta]Checking balance and fees on chain [/magenta] [blue]{subtensor.network}[/blue]" + ) + # check existential deposit and fee + logging.debug("Fetching existential and fee") + block_hash = await subtensor.substrate.get_chain_head() + account_balance, existential_deposit = await asyncio.gather( + subtensor.get_balance(wallet.coldkeypub.ss58_address, block_hash=block_hash), + subtensor.get_existential_deposit(block_hash=block_hash), + ) + + fee = await subtensor.get_transfer_fee( + wallet=wallet, dest=destination, value=amount, keep_alive=keep_alive + ) + + if not keep_alive: + # Check if the transfer should keep_alive the account + existential_deposit = Balance(0) + + # Check if we have enough balance. + if transfer_all is True: + if (account_balance - fee) < existential_deposit: + logging.error("Not enough balance to transfer") + return False + elif account_balance < (amount + fee + existential_deposit): + logging.error(":cross_mark: [red]Not enough balance[/red]") + logging.error(f"\t\tBalance:\t[blue]{account_balance}[/blue]") + logging.error(f"\t\tAmount:\t[blue]{amount}[/blue]") + logging.error(f"\t\tFor fee:\t[blue]{fee}[/blue]") + return False + + logging.info(":satellite: [magenta]Transferring... bool: + """ + Removes stake into the wallet coldkey from the specified hotkey ``uid``. + + Parameters: + subtensor: Subtensor instance. + wallet: Bittensor wallet object. + netuid: Subnet unique id. + hotkey_ss58: The ``ss58`` address of the hotkey to unstake from. + amount: Amount to stake as Bittensor balance. + allow_partial_stake: If true, allows partial unstaking if price tolerance exceeded. + safe_unstaking: If true, enables price safety checks. + rate_tolerance: Maximum allowed price decrease percentage (0.005 = 0.5%). + 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. + + Returns: + bool: True if the subnet registration was successful, False otherwise. + """ + # Decrypt keys, + if not (unlock := unlock_key(wallet)).success: + logging.error(unlock.message) + return False + + logging.info( + f":satellite: [magenta]Syncing with chain:[/magenta] [blue]{subtensor.network}[/blue] [magenta]...[/magenta]" + ) + block_hash = await subtensor.substrate.get_chain_head() + old_balance, old_stake = await asyncio.gather( + subtensor.get_balance(wallet.coldkeypub.ss58_address, block_hash=block_hash), + subtensor.get_stake( + coldkey_ss58=wallet.coldkeypub.ss58_address, + hotkey_ss58=hotkey_ss58, + netuid=netuid, + block_hash=block_hash, + ), + ) + + amount.set_unit(netuid) + + # Check enough to unstake. + if amount > old_stake: + logging.error( + f":cross_mark: [red]Not enough stake[/red]: [green]{old_stake}[/green] to unstake: " + f"[blue]{amount}[/blue] from hotkey: [yellow]{wallet.hotkey_str}[/yellow]" + ) + return False + + try: + call_params = { + "hotkey": hotkey_ss58, + "netuid": netuid, + "amount_unstaked": amount.rao, + } + if safe_unstaking: + pool = await subtensor.subnet(netuid=netuid) + base_price = pool.price.tao + + if pool.netuid == 0: + price_with_tolerance = base_price + else: + price_with_tolerance = base_price * (1 - rate_tolerance) + + logging_info = ( + f":satellite: [magenta]Safe Unstaking from:[/magenta] " + f"netuid: [green]{netuid}[/green], amount: [green]{amount}[/green], " + f"tolerance percentage: [green]{rate_tolerance * 100}%[/green], " + f"price limit: [green]{price_with_tolerance}[/green], " + f"original price: [green]{base_price}[/green], " + f"with partial unstake: [green]{allow_partial_stake}[/green] " + f"on [blue]{subtensor.network}[/blue]" + ) + + limit_price = Balance.from_tao(price_with_tolerance).rao + call_params.update( + { + "limit_price": limit_price, + "allow_partial": allow_partial_stake, + } + ) + call_function = "remove_stake_limit" + else: + logging_info = ( + f":satellite: [magenta]Unstaking from:[/magenta] " + f"netuid: [green]{netuid}[/green], amount: [green]{amount}[/green] " + f"on [blue]{subtensor.network}[/blue]" + ) + call_function = "remove_stake" + + call = await subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function=call_function, + call_params=call_params, + ) + fee = await get_extrinsic_fee( + subtensor=subtensor, call=call, keypair=wallet.coldkeypub, netuid=netuid + ) + logging.info(f"{logging_info} for fee [blue]{fee}[/blue][magenta]...[/magenta]") + success, message = await subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + nonce_key="coldkeypub", + sign_with="coldkey", + use_nonce=True, + period=period, + raise_error=raise_error, + ) + + if success: # If we successfully unstaked. + # We only wait here if we expect finalization. + if not wait_for_finalization and not wait_for_inclusion: + return True + + logging.success(":white_heavy_check_mark: [green]Finalized[/green]") + + logging.info( + f":satellite: [magenta]Checking Balance on:[/magenta] [blue]{subtensor.network}[/blue] " + f"[magenta]...[/magenta]" + ) + new_block_hash = await subtensor.substrate.get_chain_head() + new_balance, new_stake = await asyncio.gather( + subtensor.get_balance( + wallet.coldkeypub.ss58_address, block_hash=new_block_hash + ), + subtensor.get_stake( + coldkey_ss58=wallet.coldkeypub.ss58_address, + hotkey_ss58=hotkey_ss58, + netuid=netuid, + block_hash=new_block_hash, + ), + ) + logging.info( + f"Balance: [blue]{old_balance}[/blue] :arrow_right: [green]{new_balance}[/green]" + ) + logging.info( + f"Stake: [blue]{old_stake}[/blue] :arrow_right: [green]{new_stake}[/green]" + ) + return True + + if safe_unstaking and "Custom error: 8" in message: + logging.error( + ":cross_mark: [red]Failed[/red]: Price exceeded tolerance limit. Either increase price tolerance or enable partial staking." + ) + else: + logging.error(f":cross_mark: [red]Failed: {message}.[/red]") + return False + + except SubstrateRequestException as error: + logging.error( + f":cross_mark: [red]Unstake filed with error: {format_error_message(error)}[/red]" + ) + return False + + +async def unstake_all_extrinsic( + subtensor: "AsyncSubtensor", + wallet: "Wallet", + hotkey: str, + netuid: int, + rate_tolerance: Optional[float] = 0.005, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> tuple[bool, str]: + """Unstakes all TAO/Alpha associated with a hotkey from the specified subnets on the Bittensor network. + + Parameters: + subtensor: Subtensor instance. + wallet: The wallet of the stake owner. + hotkey: The SS58 address of the hotkey to unstake from. + netuid: The unique identifier of the subnet. + rate_tolerance: The maximum allowed price change ratio when unstaking. For example, 0.005 = 0.5% maximum + price decrease. If not passed (None), then unstaking goes without price limit. Default is `0.005`. + 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. + + Returns: + tuple[bool, str]: + A tuple containing: + - `True` and a success message if the unstake operation succeeded; + - `False` and an error message otherwise. + """ + if not (unlock := unlock_key(wallet)).success: + logging.error(unlock.message) + return False, unlock.message + + call_params = { + "hotkey": hotkey, + "netuid": netuid, + "limit_price": None, + } + + if rate_tolerance: + current_price = (await subtensor.subnet(netuid=netuid)).price + limit_price = current_price * (1 - rate_tolerance) + call_params.update({"limit_price": limit_price}) + + async with subtensor.substrate as substrate: + call = await substrate.compose_call( + call_module="SubtensorModule", + call_function="remove_stake_full_limit", + call_params=call_params, + ) + + return await subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + nonce_key="coldkeypub", + sign_with="coldkey", + use_nonce=True, + period=period, + raise_error=raise_error, + ) + + +async def unstake_multiple_extrinsic( + subtensor: "AsyncSubtensor", + wallet: "Wallet", + netuids: list[int], + hotkey_ss58s: list[str], + amounts: Optional[list[Balance]] = None, + unstake_all: bool = False, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> bool: + """ + Removes stake from each ``hotkey_ss58`` in the list, using each amount, to a common coldkey. + + Parameters: + subtensor: AsyncSubtensor instance. + wallet: The wallet with the coldkey to unstake to. + netuids: List of subnets unique IDs to unstake from. + hotkey_ss58s: List of hotkeys to unstake from. + amounts: List of amounts to unstake. If ``None``, unstake all. + unstake_all: If true, unstakes all tokens. Default is ``False``. + 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. + + Returns: + bool: True if the subnet registration was successful, False otherwise. + """ + # Unlock coldkey. + if not (unlock := unlock_key(wallet)).success: + logging.error(unlock.message) + return False + + # or amounts or unstake_all (no both) + if amounts and unstake_all: + raise ValueError("Cannot specify both `amounts` and `unstake_all`.") + + if amounts is not None and not all( + isinstance(amount, Balance) for amount in amounts + ): + raise TypeError("amounts must be a [list of bittensor.Balance] or None") + + if amounts is None: + amounts = [None] * len(hotkey_ss58s) + else: + # Convert to Balance + amounts = [amount.set_unit(netuid) for amount, netuid in zip(amounts, netuids)] + if sum(amount.tao for amount in amounts) == 0: + # Staking 0 tao + return True + + assert all( + [ + isinstance(netuids, list), + isinstance(hotkey_ss58s, list), + isinstance(amounts, list), + ] + ), "The `netuids`, `hotkey_ss58s` and `amounts` must be lists." + + if len(hotkey_ss58s) == 0: + return True + + assert len(netuids) == len(hotkey_ss58s) == len(amounts), ( + "The number of items in `netuids`, `hotkey_ss58s` and `amounts` must be the same." + ) + + if not all(isinstance(hotkey_ss58, str) for hotkey_ss58 in hotkey_ss58s): + raise TypeError("hotkey_ss58s must be a list of str") + + if amounts is not None and len(amounts) != len(hotkey_ss58s): + raise ValueError("amounts must be a list of the same length as hotkey_ss58s") + + if netuids is not None and len(netuids) != len(hotkey_ss58s): + raise ValueError("netuids must be a list of the same length as hotkey_ss58s") + + logging.info( + f":satellite: [magenta]Syncing with chain:[/magenta] [blue]{subtensor.network}[/blue] [magenta]...[/magenta]" + ) + + block_hash = await subtensor.substrate.get_chain_head() + + all_stakes, old_balance = await asyncio.gather( + subtensor.get_stake_for_coldkey( + coldkey_ss58=wallet.coldkeypub.ss58_address, block_hash=block_hash + ), + subtensor.get_balance(wallet.coldkeypub.ss58_address, block_hash=block_hash), + ) + + old_stakes: list[Balance] = get_old_stakes( + wallet=wallet, hotkey_ss58s=hotkey_ss58s, netuids=netuids, all_stakes=all_stakes + ) + + successful_unstakes = 0 + for idx, (hotkey_ss58, amount, old_stake, netuid) in enumerate( + zip(hotkey_ss58s, amounts, old_stakes, netuids) + ): + # Convert to bittensor.Balance + if amount is None: + # Unstake it all. + unstaking_balance = old_stake + logging.warning( + f"Didn't receive any unstaking amount. Unstaking all existing stake: [blue]{old_stake}[/blue] " + f"from hotkey: [blue]{hotkey_ss58}[/blue]" + ) + else: + unstaking_balance = amount + + # Check enough to unstake. + stake_on_uid = old_stake + if unstaking_balance > stake_on_uid: + logging.error( + f":cross_mark: [red]Not enough stake[/red]: [green]{stake_on_uid}[/green] to unstake: " + f"[blue]{unstaking_balance}[/blue] from hotkey: [blue]{wallet.hotkey_str}[/blue]." + ) + continue + + try: + call = await subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="remove_stake", + call_params={ + "hotkey": hotkey_ss58, + "amount_unstaked": unstaking_balance.rao, + "netuid": netuid, + }, + ) + fee = await get_extrinsic_fee( + subtensor=subtensor, call=call, keypair=wallet.coldkeypub, netuid=netuid + ) + logging.info( + f"Unstaking [blue]{unstaking_balance}[/blue] from hotkey: [magenta]{hotkey_ss58}[/magenta] on netuid: " + f"[blue]{netuid}[/blue] for fee [blue]{fee}[/blue]" + ) + + staking_response, err_msg = await subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + nonce_key="coldkeypub", + sign_with="coldkey", + use_nonce=True, + period=period, + raise_error=raise_error, + ) + + if staking_response is True: # If we successfully unstaked. + # We only wait here if we expect finalization. + + if not wait_for_finalization and not wait_for_inclusion: + successful_unstakes += 1 + continue + + logging.info(":white_heavy_check_mark: [green]Finalized[/green]") + + logging.info( + f":satellite: [magenta]Checking Balance on:[/magenta] [blue]{subtensor.network}[/blue] " + f"[magenta]...[/magenta]..." + ) + block_hash = await subtensor.substrate.get_chain_head() + new_stake = await subtensor.get_stake( + coldkey_ss58=wallet.coldkeypub.ss58_address, + hotkey_ss58=hotkey_ss58, + netuid=netuid, + block_hash=block_hash, + ) + logging.info( + f"Stake ({hotkey_ss58}): [blue]{stake_on_uid}[/blue] :arrow_right: [green]{new_stake}[/green]" + ) + successful_unstakes += 1 + else: + logging.error(f":cross_mark: [red]Failed: {err_msg}.[/red]") + continue + + except SubstrateRequestException as error: + logging.error( + f":cross_mark: [red]Multiple unstake filed with error: {format_error_message(error)}[/red]" + ) + return False + + if successful_unstakes != 0: + logging.info( + f":satellite: [magenta]Checking Balance on:[/magenta] [blue]{subtensor.network}[/blue] " + f"[magenta]...[/magenta]" + ) + block_hash = await subtensor.substrate.get_chain_head() + new_balance = await subtensor.get_balance( + wallet.coldkeypub.ss58_address, block_hash=block_hash + ) + logging.info( + f"Balance: [blue]{old_balance}[/blue] :arrow_right: [green]{new_balance}[/green]" + ) + return True + + return False diff --git a/bittensor/core/extrinsics/asyncex/utils.py b/bittensor/core/extrinsics/asyncex/utils.py new file mode 100644 index 0000000000..7c756e2499 --- /dev/null +++ b/bittensor/core/extrinsics/asyncex/utils.py @@ -0,0 +1,34 @@ +from typing import TYPE_CHECKING, Optional + +from bittensor.utils.balance import Balance + +if TYPE_CHECKING: + from scalecodec import GenericCall + from bittensor_wallet import Keypair + from bittensor.core.async_subtensor import AsyncSubtensor + + +async def get_extrinsic_fee( + subtensor: "AsyncSubtensor", + call: "GenericCall", + keypair: "Keypair", + netuid: Optional[int] = None, +): + """ + Get extrinsic fee for a given extrinsic call and keypair for a given SN's netuid. + + Arguments: + subtensor: The Subtensor instance. + netuid: The SN's netuid. + call: The extrinsic call. + keypair: The keypair associated with the extrinsic. + + Returns: + Balance object representing the extrinsic fee in RAO. + """ + payment_info = await subtensor.substrate.get_payment_info( + call=call, keypair=keypair + ) + return Balance.from_rao(amount=payment_info["partial_fee"]).set_unit( + netuid=netuid or 0 + ) diff --git a/bittensor/core/extrinsics/asyncex/weights.py b/bittensor/core/extrinsics/asyncex/weights.py new file mode 100644 index 0000000000..53aa650f2d --- /dev/null +++ b/bittensor/core/extrinsics/asyncex/weights.py @@ -0,0 +1,226 @@ +"""This module provides async functionality for working with weights in the Bittensor network.""" + +from typing import Union, TYPE_CHECKING, Optional + +import numpy as np +from numpy.typing import NDArray + +from bittensor.core.settings import version_as_int +from bittensor.utils import get_function_name +from bittensor.utils.btlogging import logging +from bittensor.utils.weight_utils import ( + convert_and_normalize_weights_and_uids, + convert_uids_and_weights, +) + +if TYPE_CHECKING: + from bittensor_wallet import Wallet + from bittensor.core.async_subtensor import AsyncSubtensor + from bittensor.utils.registration import torch + + +async def commit_weights_extrinsic( + subtensor: "AsyncSubtensor", + wallet: "Wallet", + netuid: int, + commit_hash: str, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = False, + wait_for_finalization: bool = False, +) -> tuple[bool, str]: + """ + Commits a hash of the neuron's weights to the Bittensor blockchain using the provided wallet. + This function is a wrapper around the `do_commit_weights` method. + + Parameters: + subtensor: The subtensor instance used for blockchain interaction. + wallet: The wallet associated with the neuron committing the weights. + netuid: The unique identifier of the subnet. + commit_hash: The hash of the neuron's weights to be committed. + 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: Whether to raise an error if the transaction fails. + 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. + + Returns: + tuple[bool, str]: + `True` if the weight commitment is successful, `False` otherwise. + `msg` is a string value describing the success or potential error. + + This function provides a user-friendly interface for committing weights to the Bittensor blockchain, ensuring proper + error handling and user interaction when required. + """ + call = await subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="commit_weights", + call_params={ + "netuid": netuid, + "commit_hash": commit_hash, + }, + ) + success, message = await subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + use_nonce=True, + period=period, + nonce_key="hotkey", + sign_with="hotkey", + raise_error=raise_error, + ) + + if success: + logging.info(message) + else: + logging.error(f"{get_function_name}: {message}") + return success, message + + +async def reveal_weights_extrinsic( + subtensor: "AsyncSubtensor", + wallet: "Wallet", + netuid: int, + uids: list[int], + weights: list[int], + salt: list[int], + version_key: int, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = False, + wait_for_finalization: bool = False, +) -> tuple[bool, str]: + """ + Reveals the weights for a specific subnet on the Bittensor blockchain using the provided wallet. + This function is a wrapper around the `_do_reveal_weights` method. + + Parameters: + subtensor: The subtensor instance used for blockchain interaction. + wallet: The wallet associated with the neuron revealing the weights. + netuid: The unique identifier of the subnet. + uids: List of neuron UIDs for which weights are being revealed. + weights: List of weight values corresponding to each UID. + salt: List of salt values corresponding to the hash function. + version_key: Version key for compatibility with the network. + 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: Whether to raise an error if the transaction fails. + 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. + + Returns: + tuple[bool, str]: + `True` if the weight commitment is successful, `False` otherwise. + `msg` is a string value describing the success or potential error. + + This function provides a user-friendly interface for revealing weights on the Bittensor blockchain, ensuring proper + error handling and user interaction when required. + """ + call = await subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="reveal_weights", + call_params={ + "netuid": netuid, + "uids": uids, + "values": weights, + "salt": salt, + "version_key": version_key, + }, + ) + success, message = await subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + sign_with="hotkey", + period=period, + nonce_key="hotkey", + use_nonce=True, + raise_error=raise_error, + ) + + if success: + logging.info(message) + else: + logging.error(f"{get_function_name}: {message}") + return success, message + + +async def set_weights_extrinsic( + subtensor: "AsyncSubtensor", + wallet: "Wallet", + netuid: int, + uids: Union[NDArray[np.int64], "torch.LongTensor", list], + weights: Union[NDArray[np.float32], "torch.FloatTensor", list], + version_key: int = version_as_int, + period: Optional[int] = 8, + raise_error: bool = False, + wait_for_inclusion: bool = False, + wait_for_finalization: bool = False, +) -> tuple[bool, str]: + """ + Sets the given weights and values on chain for a given wallet hotkey account. + + Parameters: + subtensor: Bittensor subtensor object. + wallet: Bittensor wallet object. + netuid: The ``netuid`` of the subnet to set weights for. + uids: The ``uint64`` uids of destination neurons. + weights: The weights to set. These must be ``float``s and correspond to the passed ``uid``s. + version_key: The version key of the validator. + 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: Whether to raise an error if the transaction fails. + 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. + + Returns: + tuple[bool, str]: + `True` if the weight commitment is successful, `False` otherwise. + `msg` is a string value describing the success or potential error. + """ + # Convert types. + uids, weights = convert_uids_and_weights(uids, weights) + + # Reformat and normalize. + weight_uids, weight_vals = convert_and_normalize_weights_and_uids(uids, weights) + + logging.info( + f":satellite: [magenta]Setting weights on [/magenta]" + f"[blue]{subtensor.network}[/blue] " + f"[magenta]...[/magenta]" + ) + + call = await subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="set_weights", + call_params={ + "dests": weight_uids, + "weights": weight_vals, + "netuid": netuid, + "version_key": version_key, + }, + ) + success, message = await subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + period=period, + use_nonce=True, + nonce_key="hotkey", + sign_with="hotkey", + raise_error=raise_error, + ) + + if success: + logging.info("Successfully set weights and Finalized.") + else: + logging.error(f"{get_function_name}: {message}") + + return success, message diff --git a/bittensor/core/extrinsics/children.py b/bittensor/core/extrinsics/children.py new file mode 100644 index 0000000000..5f7c1a9d84 --- /dev/null +++ b/bittensor/core/extrinsics/children.py @@ -0,0 +1,156 @@ +from typing import TYPE_CHECKING, Optional +from bittensor.utils import float_to_u64, unlock_key + +if TYPE_CHECKING: + from bittensor_wallet import Wallet + from bittensor.core.subtensor import Subtensor + + +def set_children_extrinsic( + subtensor: "Subtensor", + wallet: "Wallet", + hotkey: str, + netuid: int, + children: list[tuple[float, str]], + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +): + """ + Allows a coldkey to set children-keys. + + Parameters: + subtensor: The Subtensor client instance used for blockchain interaction. + wallet: bittensor wallet instance. + hotkey: The ``SS58`` address of the neuron's hotkey. + netuid: The netuid value. + children: A list of children with their proportions. + 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. + + Returns: + Tuple[bool, str]: + - True and a success message if the extrinsic is successfully submitted or processed. + - False and an error message if the submission fails or the wallet cannot be unlocked. + + Raises: + DuplicateChild: There are duplicates in the list of children. + InvalidChild: Child is the hotkey. + NonAssociatedColdKey: The coldkey does not own the hotkey or the child is the same as the hotkey. + NotEnoughStakeToSetChildkeys: Parent key doesn't have minimum own stake. + ProportionOverflow: The sum of the proportions does exceed uint64. + RegistrationNotPermittedOnRootSubnet: Attempting to register a child on the root network. + SubNetworkDoesNotExist: Attempting to register to a non-existent network. + TooManyChildren: Too many children in request. + TxRateLimitExceeded: Hotkey hit the rate limit. + bittensor_wallet.errors.KeyFileError: Failed to decode keyfile data. + bittensor_wallet.errors.PasswordError: Decryption failed or wrong password for decryption provided. + """ + unlock = unlock_key(wallet, raise_error=raise_error) + + if not unlock.success: + return False, unlock.message + + call = subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="set_children", + call_params={ + "children": [ + ( + float_to_u64(proportion), + child_hotkey, + ) + for proportion, child_hotkey in children + ], + "hotkey": hotkey, + "netuid": netuid, + }, + ) + + success, message = 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 wait_for_finalization and not wait_for_inclusion: + return True, message + + if success: + return True, "Success with `set_children_extrinsic` response." + + return True, message + + +def root_set_pending_childkey_cooldown_extrinsic( + subtensor: "Subtensor", + wallet: "Wallet", + cooldown: int, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, +) -> tuple[bool, str]: + """ + Allows a root coldkey to set children-keys. + + Parameters: + subtensor: The Subtensor client instance used for blockchain interaction. + wallet: The wallet used to sign the extrinsic (must be unlocked). + cooldown: The cooldown period in blocks. + 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. + + Returns: + Tuple[bool, str]: + - True and a success message if the extrinsic is successfully submitted or processed. + - False and an error message if the submission fails or the wallet cannot be unlocked. + """ + unlock = unlock_key(wallet) + + if not unlock.success: + return False, unlock.message + + call = subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="set_pending_childkey_cooldown", + call_params={"cooldown": cooldown}, + ) + + sudo_call = subtensor.substrate.compose_call( + call_module="Sudo", + call_function="sudo", + call_params={"call": call}, + ) + + success, message = subtensor.sign_and_send_extrinsic( + call=sudo_call, + wallet=wallet, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + if not wait_for_finalization and not wait_for_inclusion: + return True, message + + if success: + return ( + True, + "Success with `root_set_pending_childkey_cooldown_extrinsic` response.", + ) + + return True, message diff --git a/bittensor/core/extrinsics/commit_reveal.py b/bittensor/core/extrinsics/commit_reveal.py new file mode 100644 index 0000000000..d0997605b4 --- /dev/null +++ b/bittensor/core/extrinsics/commit_reveal.py @@ -0,0 +1,111 @@ +"""This module provides sync functionality for commit reveal in the Bittensor network.""" + +from typing import Union, TYPE_CHECKING, Optional + +import numpy as np +from bittensor_drand import get_encrypted_commit +from numpy.typing import NDArray + +from bittensor.core.settings import version_as_int +from bittensor.utils.btlogging import logging +from bittensor.utils.weight_utils import convert_and_normalize_weights_and_uids + +if TYPE_CHECKING: + from bittensor_wallet import Wallet + from bittensor.core.subtensor import Subtensor + from bittensor.utils.registration import torch + + +def commit_reveal_extrinsic( + subtensor: "Subtensor", + wallet: "Wallet", + netuid: int, + uids: Union[NDArray[np.int64], "torch.LongTensor", list], + weights: Union[NDArray[np.float32], "torch.FloatTensor", list], + block_time: Union[int, float] = 12.0, + commit_reveal_version: int = 4, + version_key: int = version_as_int, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = False, + wait_for_finalization: bool = False, +) -> tuple[bool, str]: + """ + Commits and reveals weights for a given subtensor and wallet with provided uids and weights. + + Parameters: + subtensor: The Subtensor instance. + wallet: The wallet to use for committing and revealing. + netuid: The id of the network. + uids: The uids to commit. + weights: The weights associated with the uids. + block_time: The number of seconds for block duration. + commit_reveal_version: The version of the chain commit-reveal protocol to use. + version_key: The version key to use for committing and revealing. + 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. + + Returns: + Tuple[bool, str]: + - True and a success message if the extrinsic is successfully submitted or processed. + - False and an error message if the submission fails or the wallet cannot be unlocked. + """ + uids, weights = convert_and_normalize_weights_and_uids(uids, weights) + + current_block = subtensor.get_current_block() + subnet_hyperparameters = subtensor.get_subnet_hyperparameters( + netuid, block=current_block + ) + tempo = subnet_hyperparameters.tempo + subnet_reveal_period_epochs = subnet_hyperparameters.commit_reveal_period + + # Encrypt `commit_hash` with t-lock and `get reveal_round` + commit_for_reveal, reveal_round = get_encrypted_commit( + uids=uids, + weights=weights, + version_key=version_key, + tempo=tempo, + current_block=current_block, + netuid=netuid, + subnet_reveal_period_epochs=subnet_reveal_period_epochs, + block_time=block_time, + hotkey=wallet.hotkey.public_key, + ) + + logging.info( + f"Committing weights hash [blue]{commit_for_reveal.hex()}[/blue] for subnet #[blue]{netuid}[/blue] with " + f"reveal round [blue]{reveal_round}[/blue]..." + ) + + call = subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="commit_timelocked_weights", + call_params={ + "netuid": netuid, + "commit": commit_for_reveal, + "reveal_round": reveal_round, + "commit_reveal_version": commit_reveal_version, + }, + ) + success, message = subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + sign_with="hotkey", + period=period, + raise_error=raise_error, + ) + + if not success: + logging.error(message) + return False, message + + logging.success( + f"[green]Finalized![/green] Weights committed with reveal round [blue]{reveal_round}[/blue]." + ) + return True, f"reveal_round:{reveal_round}" diff --git a/bittensor/core/extrinsics/liquidity.py b/bittensor/core/extrinsics/liquidity.py new file mode 100644 index 0000000000..dbb974082a --- /dev/null +++ b/bittensor/core/extrinsics/liquidity.py @@ -0,0 +1,249 @@ +from typing import Optional, TYPE_CHECKING + +from bittensor.utils import unlock_key +from bittensor.utils.balance import Balance +from bittensor.utils.btlogging import logging +from bittensor.utils.liquidity import price_to_tick + +if TYPE_CHECKING: + from bittensor_wallet import Wallet + from bittensor.core.subtensor import Subtensor + + +def add_liquidity_extrinsic( + subtensor: "Subtensor", + wallet: "Wallet", + netuid: int, + liquidity: Balance, + price_low: Balance, + price_high: Balance, + hotkey: Optional[str] = None, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> tuple[bool, str]: + """ + Adds liquidity to the specified price range. + + Parameters: + subtensor: The Subtensor client instance used for blockchain interaction. + wallet: The wallet used to sign the extrinsic (must be unlocked). + netuid: The UID of the target subnet for which the call is being initiated. + liquidity: The amount of liquidity to be added. + price_low: The lower bound of the price tick range. + price_high: The upper bound of the price tick range. + hotkey: The hotkey with staked TAO in Alpha. If not passed then the wallet hotkey is used. + 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. + + Returns: + Tuple[bool, str]: + - True and a success message if the extrinsic is successfully submitted or processed. + - False and an error message if the submission fails or the wallet cannot be unlocked. + + Note: Adding is allowed even when user liquidity is enabled in specified subnet. Call + `toggle_user_liquidity_extrinsic` to enable/disable user liquidity. + """ + if not (unlock := unlock_key(wallet)).success: + logging.error(unlock.message) + return False, unlock.message + + tick_low = price_to_tick(price_low.tao) + tick_high = price_to_tick(price_high.tao) + + call = subtensor.substrate.compose_call( + call_module="Swap", + call_function="add_liquidity", + call_params={ + "hotkey": hotkey or wallet.hotkey.ss58_address, + "netuid": netuid, + "tick_low": tick_low, + "tick_high": tick_high, + "liquidity": liquidity.rao, + }, + ) + + return subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + use_nonce=True, + period=period, + raise_error=raise_error, + ) + + +def modify_liquidity_extrinsic( + subtensor: "Subtensor", + wallet: "Wallet", + netuid: int, + position_id: int, + liquidity_delta: Balance, + hotkey: Optional[str] = None, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> tuple[bool, str]: + """Modifies liquidity in liquidity position by adding or removing liquidity from it. + + Parameters: + subtensor: The Subtensor client instance used for blockchain interaction. + wallet: The wallet used to sign the extrinsic (must be unlocked). + netuid: The UID of the target subnet for which the call is being initiated. + position_id: The id of the position record in the pool. + liquidity_delta: The amount of liquidity to be added or removed (add if positive or remove if negative). + hotkey: The hotkey with staked TAO in Alpha. If not passed then the wallet hotkey is used. Defaults to `None`. + 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. + + Returns: + Tuple[bool, str]: + - True and a success message if the extrinsic is successfully submitted or processed. + - False and an error message if the submission fails or the wallet cannot be unlocked. + + Note: Modifying is allowed even when user liquidity is enabled in specified subnet. Call + `toggle_user_liquidity_extrinsic` to enable/disable user liquidity. + """ + if not (unlock := unlock_key(wallet)).success: + logging.error(unlock.message) + return False, unlock.message + + call = subtensor.substrate.compose_call( + call_module="Swap", + call_function="modify_position", + call_params={ + "hotkey": hotkey or wallet.hotkey.ss58_address, + "netuid": netuid, + "position_id": position_id, + "liquidity_delta": liquidity_delta.rao, + }, + ) + + return subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + use_nonce=True, + period=period, + raise_error=raise_error, + ) + + +def remove_liquidity_extrinsic( + subtensor: "Subtensor", + wallet: "Wallet", + netuid: int, + position_id: int, + hotkey: Optional[str] = None, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> tuple[bool, str]: + """Remove liquidity and credit balances back to wallet's hotkey stake. + + Parameters: + subtensor: The Subtensor client instance used for blockchain interaction. + wallet: The wallet used to sign the extrinsic (must be unlocked). + netuid: The UID of the target subnet for which the call is being initiated. + position_id: The id of the position record in the pool. + hotkey: The hotkey with staked TAO in Alpha. If not passed then the wallet hotkey is used. Defaults to `None`. + 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. + + Returns: + Tuple[bool, str]: + - True and a success message if the extrinsic is successfully submitted or processed. + - False and an error message if the submission fails or the wallet cannot be unlocked. + + Note: Adding is allowed even when user liquidity is enabled in specified subnet. Call + `toggle_user_liquidity_extrinsic` to enable/disable user liquidity. + """ + if not (unlock := unlock_key(wallet)).success: + logging.error(unlock.message) + return False, unlock.message + + call = subtensor.substrate.compose_call( + call_module="Swap", + call_function="remove_liquidity", + call_params={ + "hotkey": hotkey or wallet.hotkey.ss58_address, + "netuid": netuid, + "position_id": position_id, + }, + ) + + return subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + use_nonce=True, + period=period, + raise_error=raise_error, + ) + + +def toggle_user_liquidity_extrinsic( + subtensor: "Subtensor", + wallet: "Wallet", + netuid: int, + enable: bool, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> tuple[bool, str]: + """Allow to toggle user liquidity for specified subnet. + + Parameters: + subtensor: The Subtensor client instance used for blockchain interaction. + wallet: The wallet used to sign the extrinsic (must be unlocked). + netuid: The UID of the target subnet for which the call is being initiated. + enable: Boolean indicating whether to enable user liquidity. + 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. + + Returns: + Tuple[bool, str]: + - True and a success message if the extrinsic is successfully submitted or processed. + - False and an error message if the submission fails or the wallet cannot be unlocked. + """ + if not (unlock := unlock_key(wallet)).success: + logging.error(unlock.message) + return False, unlock.message + + call = subtensor.substrate.compose_call( + call_module="Swap", + call_function="toggle_user_liquidity", + call_params={"netuid": netuid, "enable": enable}, + ) + + return 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, + ) diff --git a/bittensor/core/extrinsics/move_stake.py b/bittensor/core/extrinsics/move_stake.py new file mode 100644 index 0000000000..b3a9d54e95 --- /dev/null +++ b/bittensor/core/extrinsics/move_stake.py @@ -0,0 +1,420 @@ +from typing import Optional, TYPE_CHECKING + +from bittensor.utils.balance import Balance +from bittensor.utils.btlogging import logging + +if TYPE_CHECKING: + from bittensor_wallet import Wallet + from bittensor.core.subtensor import Subtensor + + +def _get_stake_in_origin_and_dest( + subtensor: "Subtensor", + origin_hotkey_ss58: str, + destination_hotkey_ss58: str, + origin_coldkey_ss58: str, + destination_coldkey_ss58: str, + origin_netuid: int, + destination_netuid: int, +) -> tuple[Balance, Balance]: + """Gets the current stake balances for both origin and destination addresses in their respective subnets.""" + block = subtensor.get_current_block() + stake_in_origin = subtensor.get_stake( + coldkey_ss58=origin_coldkey_ss58, + hotkey_ss58=origin_hotkey_ss58, + netuid=origin_netuid, + block=block, + ) + stake_in_destination = subtensor.get_stake( + coldkey_ss58=destination_coldkey_ss58, + hotkey_ss58=destination_hotkey_ss58, + netuid=destination_netuid, + block=block, + ) + return stake_in_origin, stake_in_destination + + +def transfer_stake_extrinsic( + subtensor: "Subtensor", + wallet: "Wallet", + destination_coldkey_ss58: str, + hotkey_ss58: str, + origin_netuid: int, + destination_netuid: int, + amount: Balance, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> bool: + """ + Transfers stake from one subnet to another while changing the coldkey owner. + + Parameters: + subtensor: The subtensor instance to interact with the blockchain. + wallet: The wallet containing the coldkey to authorize the transfer. + destination_coldkey_ss58: SS58 address of the destination coldkey. + hotkey_ss58: SS58 address of the hotkey associated with the stake. + origin_netuid: Network UID of the origin subnet. + destination_netuid: Network UID of the destination subnet. + amount: The amount of stake to transfer as a `Balance` object. + 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. + + Returns: + success (bool): True if the transfer was successful. + """ + + amount.set_unit(netuid=origin_netuid) + + # Check sufficient stake + stake_in_origin, stake_in_destination = _get_stake_in_origin_and_dest( + subtensor, + origin_hotkey_ss58=hotkey_ss58, + destination_hotkey_ss58=hotkey_ss58, + origin_netuid=origin_netuid, + destination_netuid=destination_netuid, + origin_coldkey_ss58=wallet.coldkeypub.ss58_address, + destination_coldkey_ss58=destination_coldkey_ss58, + ) + if stake_in_origin < amount: + logging.error( + f":cross_mark: [red]Failed[/red]: Insufficient stake in origin hotkey: {hotkey_ss58}. " + f"Stake: {stake_in_origin}, amount: {amount}" + ) + return False + + try: + logging.info( + f"Transferring stake from coldkey [blue]{wallet.coldkeypub.ss58_address}[/blue] to coldkey [" + f"blue]{destination_coldkey_ss58}[/blue]\n" + f"Amount: [green]{amount}[/green] from netuid [yellow]{origin_netuid}[/yellow] to netuid " + f"[yellow]{destination_netuid}[/yellow]" + ) + call = subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="transfer_stake", + call_params={ + "destination_coldkey": destination_coldkey_ss58, + "hotkey": hotkey_ss58, + "origin_netuid": origin_netuid, + "destination_netuid": destination_netuid, + "alpha_amount": amount.rao, + }, + ) + + success, message = 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, + ) + + if success: + if not wait_for_finalization and not wait_for_inclusion: + return True + + logging.success(":white_heavy_check_mark: [green]Finalized[/green]") + + # Get updated stakes + origin_stake, dest_stake = _get_stake_in_origin_and_dest( + subtensor=subtensor, + origin_hotkey_ss58=hotkey_ss58, + destination_hotkey_ss58=hotkey_ss58, + origin_netuid=origin_netuid, + destination_netuid=destination_netuid, + origin_coldkey_ss58=wallet.coldkeypub.ss58_address, + destination_coldkey_ss58=destination_coldkey_ss58, + ) + logging.info( + f"Origin Stake: [blue]{stake_in_origin}[/blue] :arrow_right: [green]{origin_stake}[/green]" + ) + logging.info( + f"Destination Stake: [blue]{stake_in_destination}[/blue] :arrow_right: [green]{dest_stake}[/green]" + ) + + return True + else: + logging.error(f":cross_mark: [red]Failed[/red]: {message}") + return False + + except Exception as e: + logging.error(f":cross_mark: [red]Failed[/red]: {str(e)}") + return False + + +def swap_stake_extrinsic( + subtensor: "Subtensor", + wallet: "Wallet", + hotkey_ss58: str, + origin_netuid: int, + destination_netuid: int, + amount: Balance, + safe_swapping: bool = False, + allow_partial_stake: bool = False, + rate_tolerance: float = 0.005, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> bool: + """ + Moves stake between subnets while keeping the same coldkey-hotkey pair ownership. + + Parameters: + subtensor: Subtensor instance. + wallet: The wallet to swap stake from. + hotkey_ss58: The hotkey SS58 address associated with the stake. + origin_netuid: The source subnet UID. + destination_netuid: The destination subnet UID. + amount: Amount to swap. + safe_swapping: If true, enables price safety checks to protect against price impact. + allow_partial_stake: If true, allows partial stake swaps when the full amount would exceed the price tolerance. + rate_tolerance: Maximum allowed increase in a price ratio (0.005 = 0.5%). + 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. + + + Returns: + success (bool): True if the swap was successful. + """ + + amount.set_unit(netuid=origin_netuid) + + # Check sufficient stake + stake_in_origin, stake_in_destination = _get_stake_in_origin_and_dest( + subtensor=subtensor, + origin_hotkey_ss58=hotkey_ss58, + destination_hotkey_ss58=hotkey_ss58, + origin_netuid=origin_netuid, + destination_netuid=destination_netuid, + origin_coldkey_ss58=wallet.coldkeypub.ss58_address, + destination_coldkey_ss58=wallet.coldkeypub.ss58_address, + ) + if stake_in_origin < amount: + logging.error( + f":cross_mark: [red]Failed[/red]: Insufficient stake in origin hotkey: {hotkey_ss58}. " + f"Stake: {stake_in_origin}, amount: {amount}" + ) + return False + + try: + call_params = { + "hotkey": hotkey_ss58, + "origin_netuid": origin_netuid, + "destination_netuid": destination_netuid, + "alpha_amount": amount.rao, + } + + if safe_swapping: + origin_pool = subtensor.subnet(netuid=origin_netuid) + destination_pool = subtensor.subnet(netuid=destination_netuid) + swap_rate_ratio = origin_pool.price.rao / destination_pool.price.rao + swap_rate_ratio_with_tolerance = swap_rate_ratio * (1 + rate_tolerance) + + logging.info( + f"Swapping stake with safety for hotkey [blue]{hotkey_ss58}[/blue]\n" + f"Amount: [green]{amount}[/green] from netuid [green]{origin_netuid}[/green] to netuid " + f"[green]{destination_netuid}[/green]\n" + f"Current price ratio: [green]{swap_rate_ratio:.4f}[/green], " + f"Ratio with tolerance: [green]{swap_rate_ratio_with_tolerance:.4f}[/green]" + ) + call_params.update( + { + "limit_price": swap_rate_ratio_with_tolerance, + "allow_partial": allow_partial_stake, + } + ) + call_function = "swap_stake_limit" + else: + logging.info( + f"Swapping stake for hotkey [blue]{hotkey_ss58}[/blue]\n" + f"Amount: [green]{amount}[/green] from netuid [green]{origin_netuid}[/green] to netuid " + f"[green]{destination_netuid}[/green]" + ) + call_function = "swap_stake" + + call = subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function=call_function, + call_params=call_params, + ) + + success, err_msg = 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, + ) + + if success: + if not wait_for_finalization and not wait_for_inclusion: + return True + + logging.success(":white_heavy_check_mark: [green]Finalized[/green]") + + # Get updated stakes + origin_stake, dest_stake = _get_stake_in_origin_and_dest( + subtensor=subtensor, + origin_hotkey_ss58=hotkey_ss58, + destination_hotkey_ss58=hotkey_ss58, + origin_netuid=origin_netuid, + destination_netuid=destination_netuid, + origin_coldkey_ss58=wallet.coldkeypub.ss58_address, + destination_coldkey_ss58=wallet.coldkeypub.ss58_address, + ) + logging.info( + f"Origin Stake: [blue]{stake_in_origin}[/blue] :arrow_right: [green]{origin_stake}[/green]" + ) + logging.info( + f"Destination Stake: [blue]{stake_in_destination}[/blue] :arrow_right: [green]{dest_stake}[/green]" + ) + + return True + else: + if safe_swapping and "Custom error: 8" in err_msg: + logging.error( + ":cross_mark: [red]Failed[/red]: Price ratio exceeded tolerance limit. Either increase price tolerance or enable partial staking." + ) + else: + logging.error(f":cross_mark: [red]Failed[/red]: {err_msg}") + return False + + except Exception as e: + logging.error(f":cross_mark: [red]Failed[/red]: {str(e)}") + return False + + +def move_stake_extrinsic( + subtensor: "Subtensor", + wallet: "Wallet", + origin_hotkey_ss58: str, + origin_netuid: int, + destination_hotkey_ss58: str, + destination_netuid: int, + amount: Optional[Balance] = None, + move_all_stake: bool = False, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> bool: + """ + Moves stake to a different hotkey and/or subnet while keeping the same coldkey owner. + + Parameters: + subtensor: Subtensor instance. + wallet: The wallet to move stake from. + origin_hotkey_ss58: The SS58 address of the source hotkey. + origin_netuid: The netuid of the source subnet. + destination_hotkey_ss58: The SS58 address of the destination hotkey. + destination_netuid: The netuid of the destination subnet. + amount: Amount to move. + move_all_stake: If true, moves all stake from the source hotkey to the destination hotkey. + 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. + + Returns: + success: True if the move was successful. Otherwise, False. + """ + if not amount and not move_all_stake: + logging.error( + ":cross_mark: [red]Failed[/red]: Please specify an `amount` or `move_all_stake` argument to move stake." + ) + return False + + # Check sufficient stake + stake_in_origin, stake_in_destination = _get_stake_in_origin_and_dest( + subtensor=subtensor, + origin_hotkey_ss58=origin_hotkey_ss58, + destination_hotkey_ss58=destination_hotkey_ss58, + origin_netuid=origin_netuid, + destination_netuid=destination_netuid, + origin_coldkey_ss58=wallet.coldkeypub.ss58_address, + destination_coldkey_ss58=wallet.coldkeypub.ss58_address, + ) + if move_all_stake: + amount = stake_in_origin + + elif stake_in_origin < amount: + logging.error( + f":cross_mark: [red]Failed[/red]: Insufficient stake in origin hotkey: {origin_hotkey_ss58}. " + f"Stake: {stake_in_origin}, amount: {amount}" + ) + return False + + amount.set_unit(netuid=origin_netuid) + + try: + logging.info( + f"Moving stake from hotkey [blue]{origin_hotkey_ss58}[/blue] to hotkey [blue]{destination_hotkey_ss58}[/blue]\n" + f"Amount: [green]{amount}[/green] from netuid [yellow]{origin_netuid}[/yellow] to netuid [yellow]{destination_netuid}[/yellow]" + ) + call = subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="move_stake", + call_params={ + "origin_hotkey": origin_hotkey_ss58, + "origin_netuid": origin_netuid, + "destination_hotkey": destination_hotkey_ss58, + "destination_netuid": destination_netuid, + "alpha_amount": amount.rao, + }, + ) + + success, message = 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, + ) + + if success: + if not wait_for_finalization and not wait_for_inclusion: + return True + + logging.success(":white_heavy_check_mark: [green]Finalized[/green]") + + # Get updated stakes + origin_stake, dest_stake = _get_stake_in_origin_and_dest( + subtensor=subtensor, + origin_hotkey_ss58=origin_hotkey_ss58, + destination_hotkey_ss58=destination_hotkey_ss58, + origin_netuid=origin_netuid, + destination_netuid=destination_netuid, + origin_coldkey_ss58=wallet.coldkeypub.ss58_address, + destination_coldkey_ss58=wallet.coldkeypub.ss58_address, + ) + logging.info( + f"Origin Stake: [blue]{stake_in_origin}[/blue] :arrow_right: [green]{origin_stake}[/green]" + ) + logging.info( + f"Destination Stake: [blue]{stake_in_destination}[/blue] :arrow_right: [green]{dest_stake}[/green]" + ) + + return True + else: + logging.error(f":cross_mark: [red]Failed[/red]: {message}") + return False + + except Exception as e: + logging.error(f":cross_mark: [red]Failed[/red]: {str(e)}") + return False diff --git a/bittensor/core/extrinsics/registration.py b/bittensor/core/extrinsics/registration.py new file mode 100644 index 0000000000..6f667ff7b6 --- /dev/null +++ b/bittensor/core/extrinsics/registration.py @@ -0,0 +1,479 @@ +""" +This module provides sync functionalities for registering a wallet with the subtensor network using Proof-of-Work (PoW). + +Extrinsics: +- register_extrinsic: Registers the wallet to the subnet. +- burned_register_extrinsic: Registers the wallet to chain by recycling TAO. +""" + +import time +from typing import Optional, Union, TYPE_CHECKING + +from bittensor.core.extrinsics.utils import get_extrinsic_fee +from bittensor.utils import unlock_key +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 + from bittensor.core.subtensor import Subtensor + + +def burned_register_extrinsic( + subtensor: "Subtensor", + wallet: "Wallet", + netuid: int, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> bool: + """Registers the wallet to chain by recycling TAO. + + Parameters: + subtensor: Subtensor instance. + wallet: Bittensor wallet object. + netuid: The ``netuid`` of the subnet to register on. + 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. + + Returns: + success: True if the extrinsic was successful. Otherwise, False. + """ + block = subtensor.get_current_block() + if not subtensor.subnet_exists(netuid, block=block): + logging.error( + f":cross_mark: [red]Failed error:[/red] subnet [blue]{netuid}[/blue] does not exist." + ) + return False + + if not (unlock := unlock_key(wallet)).success: + logging.error(unlock.message) + return False + + logging.info( + f":satellite: [magenta]Checking Account on subnet[/magenta] [blue]{netuid}[/blue][magenta] ...[/magenta]" + ) + neuron = subtensor.get_neuron_for_pubkey_and_subnet( + wallet.hotkey.ss58_address, netuid=netuid, block=block + ) + + old_balance = subtensor.get_balance(wallet.coldkeypub.ss58_address, block=block) + + if not neuron.is_null: + logging.info(":white_heavy_check_mark: [green]Already Registered[/green]") + logging.info(f"\t\tuid: [blue]{neuron.uid}[/blue]") + logging.info(f"\t\tnetuid: [blue]{neuron.netuid}[/blue]") + logging.info(f"\t\thotkey: [blue]{neuron.hotkey}[/blue]") + logging.info(f"\t\tcoldkey: [blue]{neuron.coldkey}[/blue]") + return True + + recycle_amount = subtensor.recycle(netuid=netuid, block=block) + logging.debug(":satellite: [magenta]Recycling TAO for Registration...[/magenta]") + logging.info(f"Recycling {recycle_amount} to register on subnet:{netuid}") + + # create extrinsic call + call = subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="burned_register", + call_params={ + "netuid": netuid, + "hotkey": wallet.hotkey.ss58_address, + }, + ) + fee = get_extrinsic_fee(subtensor=subtensor, call=call, keypair=wallet.coldkeypub) + logging.info( + f"The registration fee for SN #[blue]{netuid}[/blue] is [blue]{fee}[/blue]." + ) + success, message = 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, + ) + + if not success: + logging.error(f":cross_mark: [red]Failed error:[/red] {message}") + time.sleep(0.5) + return False + + # TODO: It is worth deleting everything below and simply returning the result without additional verification. This + # should be the responsibility of the user. We will also reduce the number of calls to the chain. + # Successful registration, final check for neuron and pubkey + logging.info(":satellite: [magenta]Checking Balance...[/magenta]") + block = subtensor.get_current_block() + new_balance = subtensor.get_balance(wallet.coldkeypub.ss58_address, block=block) + + logging.info( + 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, block=block + ) + if is_registered: + logging.info(":white_heavy_check_mark: [green]Registered[/green]") + return True + + # neuron not found, try again + logging.error(":cross_mark: [red]Unknown error. Neuron not found.[/red]") + return False + + +def register_subnet_extrinsic( + subtensor: "Subtensor", + wallet: "Wallet", + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> bool: + """ + Registers a new subnetwork on the Bittensor blockchain. + + Parameters: + subtensor: The subtensor interface to send the extrinsic. + wallet: The wallet to be used for subnet registration. + 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. + + Returns: + bool: True if the subnet registration was successful, False otherwise. + """ + balance = subtensor.get_balance(wallet.coldkeypub.ss58_address) + burn_cost = subtensor.get_subnet_burn_cost() + + if burn_cost > balance: + logging.error( + f"Insufficient balance {balance} to register subnet. Current burn cost is {burn_cost} TAO" + ) + return False + + call = subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="register_network", + call_params={ + "hotkey": wallet.hotkey.ss58_address, + "mechid": 1, + }, + ) + + success, message = 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, + ) + + if not wait_for_finalization and not wait_for_inclusion: + return True + + if success: + logging.success( + ":white_heavy_check_mark: [green]Successfully registered subnet[/green]" + ) + return True + + logging.error(f"Failed to register subnet: {message}") + return False + + +def register_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, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> bool: + """Registers a neuron on the Bittensor subnet with provided netuid using the provided wallet. + + 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. + 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. + + Returns: + bool: True if the subnet registration was successful, False otherwise. + """ + + logging.debug("[magenta]Checking subnet status... [/magenta]") + block = subtensor.get_current_block() + if not subtensor.subnet_exists(netuid, block=block): + logging.error( + f":cross_mark: [red]Failed error:[/red] subnet [blue]{netuid}[/blue] does not exist." + ) + return False + + logging.info( + f":satellite: [magenta]Checking Account on subnet[/magenta] [blue]{netuid}[/blue] [magenta]...[/magenta]" + ) + neuron = subtensor.get_neuron_for_pubkey_and_subnet( + hotkey_ss58=wallet.hotkey.ss58_address, netuid=netuid, block=block + ) + + if not neuron.is_null: + logging.info(":white_heavy_check_mark: [green]Already Registered[/green]") + logging.info(f"\t\tuid: [blue]{neuron.uid}[/blue]") + logging.info(f"\t\tnetuid: [blue]{neuron.netuid}[/blue]") + logging.info(f"\t\thotkey: [blue]{neuron.hotkey}[/blue]") + logging.info(f"\t\tcoldkey: [blue]{neuron.coldkey}[/blue]") + return True + + logging.debug( + f"Registration hotkey: {wallet.hotkey.ss58_address}, Public coldkey: " + f"{wallet.coldkey.ss58_address} in the network: {subtensor.network}." + ) + + if not torch: + log_no_torch_error() + return False + + # Attempt rolling registration. + attempts = 1 + + while True: + logging.info( + f":satellite: [magenta]Registering...[/magenta] [blue]({attempts}/{max_allowed_attempts})[/blue]" + ) + # Solve latest POW. + if cuda: + if not torch.cuda.is_available(): + return False + + 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: + 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: + logging.error( + f":white_heavy_check_mark: [green]Already registered on netuid:[/green] [blue]{netuid}[/blue]" + ) + return True + + # pow successful, proceed to submit pow to chain for registration + else: + logging.info(":satellite: [magenta]Submitting POW...[/magenta]") + # check if a pow result is still valid + while not pow_result.is_stale(subtensor=subtensor): + # create extrinsic call + call = subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="register", + call_params={ + "netuid": netuid, + "block_number": pow_result.block_number, + "nonce": pow_result.nonce, + "work": [int(byte_) for byte_ in pow_result.seal], + "hotkey": wallet.hotkey.ss58_address, + "coldkey": wallet.coldkeypub.ss58_address, + }, + ) + logging.debug( + ":satellite: [magenta]Sending POW Register Extrinsic...[/magenta]" + ) + success, message = 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, + ) + + if not success: + # Look error here + # https://github.com/opentensor/subtensor/blob/development/pallets/subtensor/src/errors.rs + + if "HotKeyAlreadyRegisteredInSubNet" in message: + logging.info( + f":white_heavy_check_mark: [green]Already Registered on subnet:[/green] " + f"[blue]{netuid}[/blue]." + ) + return True + logging.error(f":cross_mark: [red]Failed[/red]: {message}") + time.sleep(0.5) + + # Successful registration, final check for neuron and pubkey + if success: + logging.info(":satellite: Checking Registration status...") + is_registered = subtensor.is_hotkey_registered( + netuid=netuid, hotkey_ss58=wallet.hotkey.ss58_address + ) + if is_registered: + logging.success( + ":white_heavy_check_mark: [green]Registered[/green]" + ) + return True + else: + # neuron not found, try again + logging.error( + ":cross_mark: [red]Unknown error. Neuron not found.[/red]" + ) + continue + else: + # Exited loop because pow is no longer valid. + logging.error("[red]POW is stale.[/red]") + # Try again. + + if attempts < max_allowed_attempts: + # Failed registration, retry pow + attempts += 1 + logging.error( + f":satellite: [magenta]Failed registration, retrying pow ...[/magenta] " + f"[blue]({attempts}/{max_allowed_attempts})[/blue]" + ) + else: + # Failed to register after max attempts. + logging.error("[red]No more attempts.[/red]") + return False + + +def set_subnet_identity_extrinsic( + subtensor: "Subtensor", + wallet: "Wallet", + netuid: int, + subnet_name: str, + github_repo: str, + subnet_contact: str, + subnet_url: str, + logo_url: str, + discord: str, + description: str, + additional: str, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> tuple[bool, str]: + """ + Set the identity information for a given subnet. + + Parameters: + subtensor: An instance of the Subtensor class to interact with the blockchain. + wallet: A wallet instance used to sign and submit the extrinsic. + netuid: The unique ID for the subnet. + subnet_name: The name of the subnet to assign the identity information. + github_repo: URL of the GitHub repository related to the subnet. + subnet_contact: Subnet's contact information, e.g., email or contact link. + subnet_url: The URL of the subnet's primary web portal. + logo_url: The URL of the logo's primary web portal. + discord: Discord server or contact for the subnet. + description: A textual description of the subnet. + additional: Any additional metadata or information related to the subnet. + 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. + + Returns: + Tuple[bool, str]: + - True and a success message if the extrinsic is successfully submitted or processed. + - False and an error message if the submission fails or the wallet cannot be unlocked. + """ + + if not (unlock := unlock_key(wallet)).success: + logging.error(unlock.message) + return False, unlock.message + + call = subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="set_subnet_identity", + call_params={ + "hotkey": wallet.hotkey.ss58_address, + "netuid": netuid, + "subnet_name": subnet_name, + "github_repo": github_repo, + "subnet_contact": subnet_contact, + "subnet_url": subnet_url, + "logo_url": logo_url, + "discord": discord, + "description": description, + "additional": additional, + }, + ) + + success, message = 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, + ) + + if not wait_for_finalization and not wait_for_inclusion: + return True, f"Identities for subnet {netuid} are sent to the chain." + + if success: + logging.success( + f":white_heavy_check_mark: [green]Identities for subnet[/green] [blue]{netuid}[/blue] [green]are set.[/green]" + ) + return True, f"Identities for subnet {netuid} are set." + else: + logging.error( + f":cross_mark: Failed to set identity for subnet [blue]{netuid}[/blue]: {message}" + ) + return False, f"Failed to set identity for subnet {netuid}: {message}" diff --git a/bittensor/core/extrinsics/root.py b/bittensor/core/extrinsics/root.py new file mode 100644 index 0000000000..161ca53fd0 --- /dev/null +++ b/bittensor/core/extrinsics/root.py @@ -0,0 +1,139 @@ +import time +from typing import Optional, TYPE_CHECKING + +from bittensor.utils import ( + u16_normalized_float, + unlock_key, +) +from bittensor.utils.balance import Balance +from bittensor.utils.btlogging import logging + +if TYPE_CHECKING: + from bittensor_wallet import Wallet + from bittensor.core.subtensor import Subtensor + + +def _get_limits(subtensor: "Subtensor") -> tuple[int, float]: + """ + Retrieves the minimum allowed weights and maximum weight limit for the given subnet. + + These values are fetched asynchronously using `asyncio.gather` to run both requests concurrently. + + Args: + subtensor (Subtensor): The AsyncSubtensor object used to interface with the network's substrate node. + + Returns: + tuple[int, float]: A tuple containing: + - `min_allowed_weights` (int): The minimum allowed weights. + - `max_weight_limit` (float): The maximum weight limit, normalized to a float value. + """ + # Get weight restrictions. + maw = subtensor.get_hyperparameter("MinAllowedWeights", netuid=0) + mwl = subtensor.get_hyperparameter("MaxWeightsLimit", netuid=0) + min_allowed_weights = int(maw) + max_weight_limit = u16_normalized_float(int(mwl)) + return min_allowed_weights, max_weight_limit + + +def root_register_extrinsic( + subtensor: "Subtensor", + wallet: "Wallet", + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> bool: + """ + Registers the neuron to the root network. + + Parameters: + subtensor: Subtensor instance to interact with the blockchain. + wallet: Bittensor Wallet instance. + 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. + + Returns: + bool: True if the subnet registration was successful, False otherwise. + """ + netuid = 0 + logging.info( + f"Registering on netuid [blue]{netuid}[/blue] on network: [blue]{subtensor.network}[/blue]" + ) + + logging.info("Fetching recycle amount & balance.") + block = subtensor.get_current_block() + recycle_call = subtensor.get_hyperparameter( + param_name="Burn", + netuid=netuid, + block=block, + ) + balance = subtensor.get_balance( + address=wallet.coldkeypub.ss58_address, + block=block, + ) + + current_recycle = Balance.from_rao(int(recycle_call)) + + if balance < current_recycle: + logging.error( + f"[red]Insufficient balance {balance} to register neuron. " + f"Current recycle is {current_recycle} TAO[/red]." + ) + return False + + if not (unlock := unlock_key(wallet)).success: + logging.error(unlock.message) + return False + + logging.debug( + f"Checking if hotkey ([blue]{wallet.hotkey_str}[/blue]) is registered on root." + ) + is_registered = subtensor.is_hotkey_registered( + netuid=netuid, hotkey_ss58=wallet.hotkey.ss58_address + ) + if is_registered: + logging.error( + ":white_heavy_check_mark: [green]Already registered on root network.[/green]" + ) + return True + + logging.info(":satellite: [magenta]Registering to root network...[/magenta]") + call = subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="root_register", + call_params={"hotkey": wallet.hotkey.ss58_address}, + ) + success, err_msg = 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, + ) + + if not success: + logging.error(f":cross_mark: [red]Failed error:[/red] {err_msg}") + time.sleep(0.5) + return False + + # Successful registration, final check for neuron and pubkey + else: + uid = subtensor.substrate.query( + module="SubtensorModule", + storage_function="Uids", + params=[netuid, wallet.hotkey.ss58_address], + ) + if uid is not None: + logging.info( + f":white_heavy_check_mark: [green]Registered with UID[/green] [blue]{uid}[/blue]." + ) + return True + else: + # neuron not found, try again + logging.error(":cross_mark: [red]Unknown error. Neuron not found.[/red]") + return False diff --git a/bittensor/core/extrinsics/serving.py b/bittensor/core/extrinsics/serving.py new file mode 100644 index 0000000000..dd62fa3669 --- /dev/null +++ b/bittensor/core/extrinsics/serving.py @@ -0,0 +1,295 @@ +from typing import Optional, Union, TYPE_CHECKING + +from bittensor.core.errors import MetadataError +from bittensor.core.settings import version_as_int +from bittensor.core.types import AxonServeCallParams +from bittensor.utils import ( + networking as net, + unlock_key, + Certificate, +) +from bittensor.utils.btlogging import logging + +if TYPE_CHECKING: + from bittensor_wallet import Wallet + from bittensor.core.axon import Axon + from bittensor.core.subtensor import Subtensor + + +def serve_extrinsic( + subtensor: "Subtensor", + wallet: "Wallet", + ip: str, + port: int, + protocol: int, + netuid: int, + placeholder1: int = 0, + placeholder2: int = 0, + certificate: Optional[Certificate] = None, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> bool: + """ + Subscribes a Bittensor endpoint to the subtensor chain. + + Parameters: + subtensor: Subtensor instance object. + wallet: Bittensor wallet object. + ip: Endpoint host port i.e., ``192.122.31.4``. + port: Endpoint port number i.e., ``9221``. + protocol: An ``int`` representation of the protocol. + netuid: The network uid to serve on. + placeholder1: A placeholder for future use. + placeholder2: A placeholder for future use. + certificate: Certificate to use for TLS. If ``None``, no TLS will be used. + 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. + + Returns: + bool: True if the subnet registration was successful, False otherwise. + """ + # Decrypt hotkey + if not (unlock := unlock_key(wallet, "hotkey")).success: + logging.error(unlock.message) + return False + + params = AxonServeCallParams( + **{ + "version": version_as_int, + "ip": net.ip_to_int(ip), + "port": port, + "ip_type": net.ip_version(ip), + "netuid": netuid, + "hotkey": wallet.hotkey.ss58_address, + "coldkey": wallet.coldkeypub.ss58_address, + "protocol": protocol, + "placeholder1": placeholder1, + "placeholder2": placeholder2, + "certificate": certificate, + } + ) + logging.debug("Checking axon ...") + neuron = subtensor.get_neuron_for_pubkey_and_subnet( + wallet.hotkey.ss58_address, netuid=netuid + ) + neuron_up_to_date = not neuron.is_null and params == neuron + if neuron_up_to_date: + logging.debug( + f"Axon already served on: [blue]AxonInfo({wallet.hotkey.ss58_address}, {ip}:{port})[/blue]" + ) + return True + + logging.debug( + f"Serving axon with: [blue]AxonInfo({wallet.hotkey.ss58_address}, {ip}:{port})[/blue] -> " + f"[green]{subtensor.network}:{netuid}[/green]" + ) + + if params.certificate is None: + call_function = "serve_axon" + else: + call_function = "serve_axon_tls" + + call = subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function=call_function, + call_params=params.dict(), + ) + + success, message = subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + sign_with="hotkey", + period=period, + raise_error=raise_error, + ) + + if success: + logging.debug( + f"Axon served with: [blue]AxonInfo({wallet.hotkey.ss58_address}, {ip}:{port})[/blue] on " + f"[green]{subtensor.network}:{netuid}[/green]" + ) + return True + + logging.error(f"Failed: {message}") + return False + + +def serve_axon_extrinsic( + subtensor: "Subtensor", + netuid: int, + axon: "Axon", + certificate: Optional["Certificate"] = None, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> bool: + """ + Serves the axon to the network. + + Parameters: + subtensor: Subtensor instance object. + netuid (int): The ``netuid`` being served on. + axon (bittensor.core.axon.Axon): Axon to serve. + certificate (bittensor.utils.Certificate): Certificate to use for TLS. If ``None``, no TLS will be used. + Defaults to ``None``. + 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. + + Returns: + bool: True if the subnet registration was successful, False otherwise. + """ + if not (unlock := unlock_key(axon.wallet, "hotkey")).success: + logging.error(unlock.message) + return False + external_port = axon.external_port + + # ---- Get external ip ---- + if axon.external_ip is None: + try: + external_ip = net.get_external_ip() + logging.success( + f":white_heavy_check_mark: [green]Found external ip:[/green] [blue]{external_ip}[/blue]" + ) + except Exception as e: + raise ConnectionError( + f"Unable to attain your external ip. Check your internet connection. error: {e}" + ) from e + else: + external_ip = axon.external_ip + + # ---- Subscribe to chain ---- + serve_success = serve_extrinsic( + subtensor=subtensor, + wallet=axon.wallet, + ip=external_ip, + port=external_port, + protocol=4, + netuid=netuid, + certificate=certificate, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + return serve_success + + +def publish_metadata( + subtensor: "Subtensor", + wallet: "Wallet", + netuid: int, + data_type: str, + data: Union[bytes, dict], + reset_bonds: bool = False, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> bool: + """ + Publishes metadata on the Bittensor network using the specified wallet and network identifier. + + Parameters: + subtensor: The subtensor instance representing the Bittensor blockchain connection. + wallet: The wallet object used for authentication in the transaction. + netuid: Network UID on which the metadata is to be published. + data_type: The data type of the information being submitted. It should be one of the following: + ``'Sha256'``, ``'Blake256'``, ``'Keccak256'``, or ``'Raw0-128'``. This specifies the format or hashing + algorithm used for the data. + data: The actual metadata content to be published. This should be formatted or hashed + according to the ``type`` specified. (Note: max ``str`` length is 128 bytes for ``'Raw0-128'``.) + reset_bonds: If `True`, the function will reset the bonds for the neuron. Defaults to `False`. + 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. + + Returns: + bool: True if the subnet registration was successful, False otherwise. + + Raises: + MetadataError: If there is an error in submitting the extrinsic, or if the response from the blockchain indicates + failure. + """ + + if not (unlock := unlock_key(wallet, "hotkey")).success: + logging.error(unlock.message) + return False + + fields = [{f"{data_type}": data}] + if reset_bonds: + fields.append({"ResetBondsFlag": b""}) + + call = subtensor.substrate.compose_call( + call_module="Commitments", + call_function="set_commitment", + call_params={ + "netuid": netuid, + "info": {"fields": [fields]}, + }, + ) + + success, message = subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + sign_with="hotkey", + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + period=period, + raise_error=raise_error, + ) + + if success: + return True + raise MetadataError(message) + + +def get_metadata( + subtensor: "Subtensor", netuid: int, hotkey: str, block: Optional[int] = None +) -> bytes: + """Fetches metadata from the blockchain for a given hotkey and netuid.""" + commit_data = subtensor.substrate.query( + module="Commitments", + storage_function="CommitmentOf", + params=[netuid, hotkey], + block_hash=subtensor.determine_block_hash(block), + ) + return commit_data + + +def get_last_bonds_reset( + subtensor: "Subtensor", netuid: int, hotkey: str, block: Optional[int] = None +) -> bytes: + """ + Fetches the last bonds reset triggered at commitment from the blockchain for a given hotkey and netuid. + + Parameters: + subtensor: Subtensor instance object. + netuid: The network uid to fetch from. + hotkey: The hotkey of the neuron for which to fetch the last bonds reset. + block: The block number to query. If ``None``, the latest block is used. + + Returns: + bytes: The last bonds reset data for the specified hotkey and netuid. + """ + return subtensor.substrate.query( + module="Commitments", + storage_function="LastBondsReset", + params=[netuid, hotkey], + block_hash=subtensor.determine_block_hash(block), + ) diff --git a/bittensor/core/extrinsics/staking.py b/bittensor/core/extrinsics/staking.py new file mode 100644 index 0000000000..eefe5ea65d --- /dev/null +++ b/bittensor/core/extrinsics/staking.py @@ -0,0 +1,369 @@ +from typing import Optional, TYPE_CHECKING, Sequence + +from async_substrate_interface.errors import SubstrateRequestException + +from bittensor.core.extrinsics.utils import get_old_stakes +from bittensor.utils import unlock_key, format_error_message +from bittensor.utils.balance import Balance +from bittensor.utils.btlogging import logging + +if TYPE_CHECKING: + from bittensor_wallet import Wallet + from bittensor.core.subtensor import Subtensor + + +def add_stake_extrinsic( + subtensor: "Subtensor", + wallet: "Wallet", + netuid: int, + hotkey_ss58: str, + amount: Balance, + safe_staking: bool = False, + allow_partial_stake: bool = False, + rate_tolerance: float = 0.005, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> bool: + """ + Adds a stake from the specified wallet to the neuron identified by the SS58 address of its hotkey in specified subnet. + Staking is a fundamental process in the Bittensor network that enables neurons to participate actively and earn + incentives. + + Parameters: + subtensor: Subtensor instance with the connection to the chain. + wallet: Bittensor wallet object. + netuid: The unique identifier of the subnet to which the neuron belongs. + hotkey_ss58: The `ss58` address of the hotkey account to stake to default to the wallet's hotkey. + amount: Amount to stake as Bittensor balance in TAO always. + safe_staking: If True, enables price safety checks. Default is ``False``. + allow_partial_stake: If True, allows partial unstaking if price tolerance exceeded. Default is ``False``. + rate_tolerance: Maximum allowed price increase percentage (0.005 = 0.5%). Default is ``0.005``. + 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. + + Returns: + bool: True if the subnet registration was successful, False otherwise. + + Raises: + SubstrateRequestException: Raised if the extrinsic fails to be included in the block within the timeout. + """ + + # Decrypt keys, + if not (unlock := unlock_key(wallet)).success: + logging.error(unlock.message) + return False + + logging.info( + f":satellite: [magenta]Syncing with chain:[/magenta] [blue]{subtensor.network}[/blue] [magenta]...[/magenta]" + ) + old_balance = subtensor.get_balance(wallet.coldkeypub.ss58_address) + block = subtensor.get_current_block() + + # Get current stake and existential deposit + old_stake = subtensor.get_stake( + hotkey_ss58=hotkey_ss58, + coldkey_ss58=wallet.coldkeypub.ss58_address, + netuid=netuid, + block=block, + ) + existential_deposit = subtensor.get_existential_deposit(block=block) + + # Leave existential balance to keep key alive. + if amount > old_balance - existential_deposit: + # If we are staking all, we need to leave at least the existential deposit. + amount = old_balance - existential_deposit + + # Check enough to stake. + if amount > old_balance: + logging.error(":cross_mark: [red]Not enough stake:[/red]") + logging.error(f"\t\tbalance:{old_balance}") + logging.error(f"\t\tamount: {amount}") + logging.error(f"\t\twallet: {wallet.name}") + return False + + call_params = { + "hotkey": hotkey_ss58, + "netuid": netuid, + "amount_staked": amount.rao, + } + + if safe_staking: + pool = subtensor.subnet(netuid=netuid) + base_price = pool.price.tao + + price_with_tolerance = ( + base_price if pool.netuid == 0 else base_price * (1 + rate_tolerance) + ) + + logging.info( + f":satellite: [magenta]Safe Staking to:[/magenta] " + f"[blue]netuid: [green]{netuid}[/green], amount: [green]{amount}[/green], " + f"tolerance percentage: [green]{rate_tolerance * 100}%[/green], " + f"price limit: [green]{price_with_tolerance}[/green], " + f"original price: [green]{base_price}[/green], " + f"with partial stake: [green]{allow_partial_stake}[/green] " + f"on [blue]{subtensor.network}[/blue][/magenta]...[/magenta]" + ) + + limit_price = Balance.from_tao(price_with_tolerance).rao + call_params.update( + { + "limit_price": limit_price, + "allow_partial": allow_partial_stake, + } + ) + call_function = "add_stake_limit" + else: + logging.info( + f":satellite: [magenta]Staking to:[/magenta] " + f"[blue]netuid: [green]{netuid}[/green], amount: [green]{amount}[/green] " + f"on [blue]{subtensor.network}[/blue][magenta]...[/magenta]" + ) + call_function = "add_stake" + + call = subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function=call_function, + call_params=call_params, + ) + + success, message = subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + use_nonce=True, + sign_with="coldkey", + nonce_key="coldkeypub", + period=period, + raise_error=raise_error, + ) + # If we successfully staked. + if success: + # We only wait here if we expect finalization. + if not wait_for_finalization and not wait_for_inclusion: + return True + + logging.success(":white_heavy_check_mark: [green]Finalized[/green]") + + logging.info( + f":satellite: [magenta]Checking Balance on:[/magenta] " + f"[blue]{subtensor.network}[/blue] [magenta]...[/magenta]" + ) + new_block = subtensor.get_current_block() + new_balance = subtensor.get_balance( + wallet.coldkeypub.ss58_address, block=new_block + ) + new_stake = subtensor.get_stake( + coldkey_ss58=wallet.coldkeypub.ss58_address, + hotkey_ss58=hotkey_ss58, + netuid=netuid, + block=new_block, + ) + logging.info( + f"Balance: [blue]{old_balance}[/blue] :arrow_right: {new_balance}[/green]" + ) + logging.info( + f"Stake: [blue]{old_stake}[/blue] :arrow_right: [green]{new_stake}[/green]" + ) + return True + + if safe_staking and "Custom error: 8" in message: + logging.error( + ":cross_mark: [red]Failed[/red]: Price exceeded tolerance limit. Either increase price tolerance or enable partial staking." + ) + else: + logging.error(f":cross_mark: [red]Failed: {message}.[/red]") + return False + + +def add_stake_multiple_extrinsic( + subtensor: "Subtensor", + wallet: "Wallet", + hotkey_ss58s: list[str], + netuids: list[int], + amounts: list[Balance], + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> bool: + """ + Adds stake to each ``hotkey_ss58`` in the list, using each amount, from a common coldkey on subnet with + corresponding netuid. + + Parameters: + subtensor: Subtensor instance with the connection to the chain. + wallet: Bittensor wallet object for the coldkey. + netuids: List of netuids to stake to. + hotkey_ss58s: List of hotkeys to stake to. + amounts: List of corresponding TAO amounts to bet for each netuid and hotkey. + 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. + + Returns: + bool: True if the subnet registration was successful, False otherwise. + """ + # Decrypt keys, + if not (unlock := unlock_key(wallet)).success: + logging.error(unlock.message) + return False + + assert all( + [ + isinstance(netuids, list), + isinstance(hotkey_ss58s, list), + isinstance(amounts, list), + ] + ), "The `netuids`, `hotkey_ss58s` and `amounts` must be lists." + + if len(hotkey_ss58s) == 0: + return True + + assert len(netuids) == len(hotkey_ss58s) == len(amounts), ( + "The number of items in `netuids`, `hotkey_ss58s` and `amounts` must be the same." + ) + + if not all(isinstance(hotkey_ss58, str) for hotkey_ss58 in hotkey_ss58s): + raise TypeError("hotkey_ss58s must be a list of str") + + new_amounts: Sequence[Optional[Balance]] = [ + amount.set_unit(netuid) for amount, netuid in zip(amounts, netuids) + ] + + if sum(amount.tao for amount in new_amounts) == 0: + # Staking 0 tao + return True + + logging.info( + f":satellite: [magenta]Syncing with chain:[/magenta] [blue]{subtensor.network}[/blue] [magenta]...[/magenta]" + ) + block = subtensor.get_current_block() + all_stakes = subtensor.get_stake_for_coldkey( + coldkey_ss58=wallet.coldkeypub.ss58_address, + ) + old_stakes: list[Balance] = get_old_stakes( + wallet=wallet, hotkey_ss58s=hotkey_ss58s, netuids=netuids, all_stakes=all_stakes + ) + + # Remove existential balance to keep key alive. + # Keys must maintain a balance of at least 1000 rao to stay alive. + total_staking_rao = sum( + [amount.rao if amount is not None else 0 for amount in new_amounts] + ) + old_balance = initial_balance = subtensor.get_balance( + address=wallet.coldkeypub.ss58_address, block=block + ) + if total_staking_rao == 0: + # Staking all to the first wallet. + if old_balance.rao > 1000: + old_balance -= Balance.from_rao(1000) + + elif total_staking_rao < 1000: + # Staking less than 1000 rao to the wallets. + pass + else: + # Staking more than 1000 rao to the wallets. + # Reduce the amount to stake to each wallet to keep the balance above 1000 rao. + percent_reduction = 1 - (1000 / total_staking_rao) + new_amounts = [ + Balance.from_tao(amount.tao * percent_reduction) for amount in new_amounts + ] + + successful_stakes = 0 + for idx, (hotkey_ss58, amount, old_stake, netuid) in enumerate( + zip(hotkey_ss58s, new_amounts, old_stakes, netuids) + ): + # Check enough to stake + if amount > old_balance: + logging.error( + f":cross_mark: [red]Not enough balance[/red]: [green]{old_balance}[/green] to stake: " + f"[blue]{amount}[/blue] from wallet: [white]{wallet.name}[/white]" + ) + continue + + try: + logging.info( + f"Staking [blue]{amount}[/blue] to hotkey: [magenta]{hotkey_ss58}[/magenta] on netuid: " + f"[blue]{netuid}[/blue]" + ) + call = subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="add_stake", + call_params={ + "hotkey": hotkey_ss58, + "amount_staked": amount.rao, + "netuid": netuid, + }, + ) + success, message = subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + use_nonce=True, + nonce_key="coldkeypub", + sign_with="coldkey", + period=period, + raise_error=raise_error, + ) + + # If we successfully staked. + if success: + # We only wait here if we expect finalization. + if not wait_for_finalization and not wait_for_inclusion: + old_balance -= amount + successful_stakes += 1 + continue + + logging.success(":white_heavy_check_mark: [green]Finalized[/green]") + + new_block = subtensor.get_current_block() + new_stake = subtensor.get_stake( + coldkey_ss58=wallet.coldkeypub.ss58_address, + hotkey_ss58=hotkey_ss58, + netuid=netuid, + block=new_block, + ) + new_balance = subtensor.get_balance( + wallet.coldkeypub.ss58_address, block=new_block + ) + logging.info( + f"Stake ({hotkey_ss58}) on netuid {netuid}: [blue]{old_stake}[/blue] :arrow_right: [green]{new_stake}[/green]" + ) + logging.info( + f"Balance: [blue]{old_balance}[/blue] :arrow_right: [green]{new_balance}[/green]" + ) + old_balance = new_balance + successful_stakes += 1 + else: + logging.error(f":cross_mark: [red]Failed[/red]: {message}") + continue + + except SubstrateRequestException as error: + logging.error( + f":cross_mark: [red]Add Stake Multiple error: {format_error_message(error)}[/red]" + ) + + if successful_stakes != 0: + logging.info( + f":satellite: [magenta]Checking Balance on:[/magenta] [blue]{subtensor.network}[/blue] " + f"[magenta]...[/magenta]" + ) + new_balance = subtensor.get_balance(wallet.coldkeypub.ss58_address) + logging.info( + f"Balance: [blue]{initial_balance}[/blue] :arrow_right: [green]{new_balance}[/green]" + ) + return True + + return False diff --git a/bittensor/core/extrinsics/start_call.py b/bittensor/core/extrinsics/start_call.py new file mode 100644 index 0000000000..4a0b37e382 --- /dev/null +++ b/bittensor/core/extrinsics/start_call.py @@ -0,0 +1,65 @@ +from typing import TYPE_CHECKING, Optional + +from bittensor.utils import unlock_key +from bittensor.utils.btlogging import logging + +if TYPE_CHECKING: + from bittensor_wallet import Wallet + from bittensor.core.subtensor import Subtensor + + +def start_call_extrinsic( + subtensor: "Subtensor", + wallet: "Wallet", + netuid: int, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, +) -> tuple[bool, str]: + """ + Submits a start_call extrinsic to the blockchain, to trigger the start call process for a subnet (used to start a + new subnet's emission mechanism). + + Parameters: + subtensor (Subtensor): The Subtensor client instance used for blockchain interaction. + wallet (Wallet): The wallet used to sign the extrinsic (must be unlocked). + netuid (int): The UID of the target subnet for which the call is being initiated. + 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 extrinsic to be included in a block. + wait_for_finalization: Whether to wait for finalization of the extrinsic. + + Returns: + Tuple[bool, str]: + - True and a success message if the extrinsic is successfully submitted or processed. + - False and an error message if the submission fails or the wallet cannot be unlocked. + """ + if not (unlock := unlock_key(wallet)).success: + logging.error(unlock.message) + return False, unlock.message + + start_call = subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="start_call", + call_params={"netuid": netuid}, + ) + + success, message = subtensor.sign_and_send_extrinsic( + call=start_call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + period=period, + raise_error=raise_error, + ) + + if not wait_for_finalization and not wait_for_inclusion: + return True, message + + if success: + return True, "Success with `start_call` response." + + return True, message diff --git a/bittensor/core/extrinsics/take.py b/bittensor/core/extrinsics/take.py new file mode 100644 index 0000000000..3effd7699b --- /dev/null +++ b/bittensor/core/extrinsics/take.py @@ -0,0 +1,115 @@ +from typing import TYPE_CHECKING, Optional + +from bittensor_wallet.bittensor_wallet import Wallet + +from bittensor.utils import unlock_key + +if TYPE_CHECKING: + from bittensor.core.subtensor import Subtensor + + +def increase_take_extrinsic( + subtensor: "Subtensor", + wallet: Wallet, + hotkey_ss58: str, + take: int, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> tuple[bool, str]: + """Sets the delegate 'take' percentage for a neuron identified by its hotkey. + + Parameters: + subtensor: The Subtensor instance. + wallet: The wallet to sign the extrinsic. + hotkey_ss58: SS58 address of the hotkey to set take for. + take: The percentage of rewards that the delegate claims from nominators. + 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. + + Returns: + Tuple[bool, str]: + - True and a success message if the extrinsic is successfully submitted or processed. + - False and an error message if the submission fails or the wallet cannot be unlocked. + """ + unlock = unlock_key(wallet, raise_error=raise_error) + + if not unlock.success: + return False, unlock.message + + call = subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="increase_take", + call_params={ + "hotkey": hotkey_ss58, + "take": take, + }, + ) + + return 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, + ) + + +def decrease_take_extrinsic( + subtensor: "Subtensor", + wallet: Wallet, + hotkey_ss58: str, + take: int, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> tuple[bool, str]: + """ + Sets the delegate `take` percentage for a neuron identified by its hotkey. + + Parameters: + subtensor: The Subtensor instance. + wallet: The wallet to sign the extrinsic. + hotkey_ss58: SS58 address of the hotkey to set take for. + take: The percentage of rewards that the delegate claims from nominators. + 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. + + Returns: + Tuple[bool, str]: + - True and a success message if the extrinsic is successfully submitted or processed. + - False and an error message if the submission fails or the wallet cannot be unlocked. + """ + unlock = unlock_key(wallet, raise_error=raise_error) + + if not unlock.success: + return False, unlock.message + + call = subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="decrease_take", + call_params={ + "hotkey": hotkey_ss58, + "take": take, + }, + ) + + return 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, + ) diff --git a/bittensor/core/extrinsics/transfer.py b/bittensor/core/extrinsics/transfer.py new file mode 100644 index 0000000000..7d2a28ad9f --- /dev/null +++ b/bittensor/core/extrinsics/transfer.py @@ -0,0 +1,141 @@ +from typing import TYPE_CHECKING, Optional + +from bittensor.core.settings import NETWORK_EXPLORER_MAP +from bittensor.utils import ( + get_explorer_url_for_network, + get_transfer_fn_params, + is_valid_bittensor_address_or_public_key, + unlock_key, +) +from bittensor.utils.balance import Balance +from bittensor.utils.btlogging import logging + +if TYPE_CHECKING: + from bittensor_wallet import Wallet + from bittensor.core.subtensor import Subtensor + + +def transfer_extrinsic( + subtensor: "Subtensor", + wallet: "Wallet", + destination: str, + amount: Optional[Balance], + keep_alive: bool = True, + transfer_all: bool = False, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> bool: + """Transfers funds from this wallet to the destination public key address. + + Parameters: + subtensor: the Subtensor object used for transfer + wallet: The wallet to sign the extrinsic. + destination: Destination public key address (ss58_address or ed25519) of recipient. + amount: Amount to stake as Bittensor balance. `None` if transferring all. + transfer_all: Whether to transfer all funds from this wallet to the destination address. + keep_alive: If set, keeps the account alive by keeping the balance above the existential deposit. + 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. + + Returns: + bool: True if the subnet registration was successful, False otherwise. + """ + if amount is None and not transfer_all: + logging.error("If not transferring all, `amount` must be specified.") + return False + + # Validate destination address. + if not is_valid_bittensor_address_or_public_key(destination): + logging.error( + f":cross_mark: [red]Invalid destination SS58 address[/red]: {destination}" + ) + return False + + logging.info(f"Initiating transfer on network: {subtensor.network}") + # Unlock wallet coldkey. + if not (unlock := unlock_key(wallet)).success: + logging.error(unlock.message) + return False + + # Check balance. + logging.info( + f":satellite: [magenta]Checking balance and fees on chain [/magenta] [blue]{subtensor.network}[/blue]" + ) + # check existential deposit and fee + logging.debug("Fetching existential and fee") + block = subtensor.get_current_block() + account_balance = subtensor.get_balance(wallet.coldkeypub.ss58_address, block=block) + if not keep_alive: + # Check if the transfer should keep_alive the account + existential_deposit = Balance(0) + else: + existential_deposit = subtensor.get_existential_deposit(block=block) + + fee = subtensor.get_transfer_fee( + wallet=wallet, dest=destination, value=amount, keep_alive=keep_alive + ) + + # Check if we have enough balance. + if transfer_all is True: + if (account_balance - fee) < existential_deposit: + logging.error("Not enough balance to transfer") + return False + elif account_balance < (amount + fee + existential_deposit): + logging.error(":cross_mark: [red]Not enough balance[/red]") + logging.error(f"\t\tBalance:\t[blue]{account_balance}[/blue]") + logging.error(f"\t\tAmount:\t[blue]{amount}[/blue]") + logging.error(f"\t\tFor fee:\t[blue]{fee}[/blue]") + return False + + logging.info(":satellite: [magenta]Transferring...[/magenta]") + + call_function, call_params = get_transfer_fn_params(amount, destination, keep_alive) + + call = subtensor.substrate.compose_call( + call_module="Balances", + call_function=call_function, + call_params=call_params, + ) + + success, message = 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, + ) + + if success: + block_hash = subtensor.get_block_hash() + logging.success(":white_heavy_check_mark: [green]Finalized[/green]") + logging.info(f"[green]Block Hash:[/green] [blue]{block_hash}[/blue]") + + if subtensor.network == "finney": + logging.debug("Fetching explorer URLs") + explorer_urls = get_explorer_url_for_network( + subtensor.network, block_hash, NETWORK_EXPLORER_MAP + ) + if explorer_urls: + logging.info( + f"[green]Opentensor Explorer Link: {explorer_urls.get('opentensor')}[/green]" + ) + logging.info( + f"[green]Taostats Explorer Link: {explorer_urls.get('taostats')}[/green]" + ) + + logging.info(":satellite: [magenta]Checking Balance...[magenta]") + new_balance = subtensor.get_balance(wallet.coldkeypub.ss58_address) + logging.info( + f"Balance: [blue]{account_balance}[/blue] :arrow_right: [green]{new_balance}[/green]" + ) + return True + + logging.error(f":cross_mark: [red]Failed[/red]: {message}") + return False diff --git a/bittensor/core/extrinsics/unstaking.py b/bittensor/core/extrinsics/unstaking.py new file mode 100644 index 0000000000..d9539a8268 --- /dev/null +++ b/bittensor/core/extrinsics/unstaking.py @@ -0,0 +1,447 @@ +from typing import Optional, TYPE_CHECKING + +from async_substrate_interface.errors import SubstrateRequestException +from bittensor.core.extrinsics.utils import get_extrinsic_fee + +from bittensor.core.extrinsics.utils import get_old_stakes +from bittensor.utils import unlock_key, format_error_message +from bittensor.utils.balance import Balance +from bittensor.utils.btlogging import logging + +if TYPE_CHECKING: + from bittensor_wallet import Wallet + from bittensor.core.subtensor import Subtensor + + +def unstake_extrinsic( + subtensor: "Subtensor", + wallet: "Wallet", + netuid: int, + hotkey_ss58: str, + amount: Balance, + allow_partial_stake: bool = False, + rate_tolerance: float = 0.005, + safe_unstaking: bool = False, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> bool: + """ + Removes stake into the wallet coldkey from the specified hotkey ``uid``. + + Parameters: + subtensor: Subtensor instance. + wallet: Bittensor wallet object. + hotkey_ss58: The ``ss58`` address of the hotkey to unstake from. + netuid: Subnet unique id. + amount: Amount to stake as Bittensor balance. + allow_partial_stake: If true, allows partial unstaking if price tolerance exceeded. + rate_tolerance: Maximum allowed price decrease percentage (0.005 = 0.5%). + safe_unstaking: If true, enables price safety checks. + 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. + + Returns: + bool: True if the subnet registration was successful, False otherwise. + """ + # Decrypt keys, + if not (unlock := unlock_key(wallet)).success: + logging.error(unlock.message) + return False + + logging.info( + f":satellite: [magenta]Syncing with chain:[/magenta] [blue]{subtensor.network}[/blue] [magenta]...[/magenta]" + ) + block = subtensor.get_current_block() + old_balance = subtensor.get_balance(wallet.coldkeypub.ss58_address, block=block) + old_stake = subtensor.get_stake( + coldkey_ss58=wallet.coldkeypub.ss58_address, + hotkey_ss58=hotkey_ss58, + netuid=netuid, + block=block, + ) + + # unstaking_balance = amount + amount.set_unit(netuid) + + # Check enough to unstake. + if amount > old_stake: + logging.error( + f":cross_mark: [red]Not enough stake[/red]: [green]{old_stake}[/green] to unstake: " + f"[blue]{amount}[/blue] from hotkey: [yellow]{wallet.hotkey_str}[/yellow]" + ) + return False + + try: + call_params = { + "hotkey": hotkey_ss58, + "netuid": netuid, + "amount_unstaked": amount.rao, + } + + if safe_unstaking: + pool = subtensor.subnet(netuid=netuid) + base_price = pool.price.tao + + if pool.netuid == 0: + price_with_tolerance = base_price + else: + price_with_tolerance = base_price * (1 - rate_tolerance) + + logging_info = ( + f":satellite: [magenta]Safe Unstaking from:[/magenta] " + f"netuid: [green]{netuid}[/green], amount: [green]{amount}[/green], " + f"tolerance percentage: [green]{rate_tolerance * 100}%[/green], " + f"price limit: [green]{price_with_tolerance}[/green], " + f"original price: [green]{base_price}[/green], " + f"with partial unstake: [green]{allow_partial_stake}[/green] " + f"on [blue]{subtensor.network}[/blue]" + ) + + limit_price = Balance.from_tao(price_with_tolerance).rao + call_params.update( + { + "limit_price": limit_price, + "allow_partial": allow_partial_stake, + } + ) + call_function = "remove_stake_limit" + else: + logging_info = ( + f":satellite: [magenta]Unstaking from:[/magenta] " + f"netuid: [green]{netuid}[/green], amount: [green]{amount}[/green] " + f"on [blue]{subtensor.network}[/blue]" + ) + call_function = "remove_stake" + + call = subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function=call_function, + call_params=call_params, + ) + fee = get_extrinsic_fee( + subtensor=subtensor, netuid=netuid, call=call, keypair=wallet.coldkeypub + ) + logging.info(f"{logging_info} for fee [blue]{fee}[/blue][magenta]...[/magenta]") + + success, message = subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + nonce_key="coldkeypub", + sign_with="coldkey", + use_nonce=True, + period=period, + raise_error=raise_error, + ) + + if success: # If we successfully unstaked. + # We only wait here if we expect finalization. + if not wait_for_finalization and not wait_for_inclusion: + return True + + logging.success(":white_heavy_check_mark: [green]Finalized[/green]") + + logging.info( + f":satellite: [magenta]Checking Balance on:[/magenta] [blue]{subtensor.network}[/blue] " + f"[magenta]...[/magenta]" + ) + block = subtensor.get_current_block() + new_balance = subtensor.get_balance( + wallet.coldkeypub.ss58_address, block=block + ) + new_stake = subtensor.get_stake( + coldkey_ss58=wallet.coldkeypub.ss58_address, + hotkey_ss58=hotkey_ss58, + netuid=netuid, + block=block, + ) + logging.info( + f"Balance: [blue]{old_balance}[/blue] :arrow_right: [green]{new_balance}[/green]" + ) + logging.info( + f"Stake: [blue]{old_stake}[/blue] :arrow_right: [green]{new_stake}[/green]" + ) + return True + + if safe_unstaking and "Custom error: 8" in message: + logging.error( + ":cross_mark: [red]Failed[/red]: Price exceeded tolerance limit. Either increase price tolerance or" + " enable partial staking." + ) + else: + logging.error(f":cross_mark: [red]Failed: {message}.[/red]") + return False + + except SubstrateRequestException as error: + logging.error( + f":cross_mark: [red]Unstake filed with error: {format_error_message(error)}[/red]" + ) + return False + + +def unstake_all_extrinsic( + subtensor: "Subtensor", + wallet: "Wallet", + hotkey: str, + netuid: int, + rate_tolerance: Optional[float] = 0.005, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> tuple[bool, str]: + """Unstakes all TAO/Alpha associated with a hotkey from the specified subnets on the Bittensor network. + + Parameters: + subtensor: Subtensor instance. + wallet: The wallet of the stake owner. + hotkey: The SS58 address of the hotkey to unstake from. + netuid: The unique identifier of the subnet. + rate_tolerance: The maximum allowed price change ratio when unstaking. For example, 0.005 = 0.5% maximum + price decrease. If not passed (None), then unstaking goes without price limit. Default is `0.005`. + 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. + + Returns: + tuple[bool, str]: + A tuple containing: + - `True` and a success message if the unstake operation succeeded; + - `False` and an error message otherwise. + """ + if not (unlock := unlock_key(wallet)).success: + logging.error(unlock.message) + return False, unlock.message + + call_params = { + "hotkey": hotkey, + "netuid": netuid, + "limit_price": None, + } + + if rate_tolerance: + current_price = subtensor.subnet(netuid=netuid).price + limit_price = current_price * (1 - rate_tolerance) + call_params.update({"limit_price": limit_price}) + + call = subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="remove_stake_full_limit", + call_params=call_params, + ) + + success, message = subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + nonce_key="coldkeypub", + sign_with="coldkey", + use_nonce=True, + period=period, + raise_error=raise_error, + ) + + return success, message + + +def unstake_multiple_extrinsic( + subtensor: "Subtensor", + wallet: "Wallet", + netuids: list[int], + hotkey_ss58s: list[str], + amounts: Optional[list[Balance]] = None, + unstake_all: bool = False, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> bool: + """ + Removes stake from each ``hotkey_ss58`` in the list, using each amount, to a common coldkey. + + Parameters: + subtensor: Subtensor instance. + wallet: The wallet with the coldkey to unstake to. + netuids: List of subnets unique IDs to unstake from. + hotkey_ss58s: List of hotkeys to unstake from. + amounts: List of amounts to unstake. If ``None``, unstake all. + unstake_all: If true, unstakes all tokens. Default is ``False``. + 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. + + Returns: + bool: True if the subnet registration was successful, False otherwise. + """ + # Unlock coldkey. + if not (unlock := unlock_key(wallet)).success: + logging.error(unlock.message) + return False + + # or amounts or unstake_all (no both) + if amounts and unstake_all: + raise ValueError("Cannot specify both `amounts` and `unstake_all`.") + + if amounts is not None and not all( + isinstance(amount, Balance) for amount in amounts + ): + raise TypeError("amounts must be a [list of bittensor.Balance] or None") + + if amounts is None: + amounts = [None] * len(hotkey_ss58s) + else: + # Convert to Balance + amounts = [amount.set_unit(netuid) for amount, netuid in zip(amounts, netuids)] + if sum(amount.tao for amount in amounts) == 0: + # Staking 0 tao + return True + + assert all( + [ + isinstance(netuids, list), + isinstance(hotkey_ss58s, list), + isinstance(amounts, list), + ] + ), "The `netuids`, `hotkey_ss58s` and `amounts` must be lists." + + if len(hotkey_ss58s) == 0: + return True + + assert len(netuids) == len(hotkey_ss58s) == len(amounts), ( + "The number of items in `netuids`, `hotkey_ss58s` and `amounts` must be the same." + ) + + if not all(isinstance(hotkey_ss58, str) for hotkey_ss58 in hotkey_ss58s): + raise TypeError("hotkey_ss58s must be a list of str") + + if amounts is not None and len(amounts) != len(hotkey_ss58s): + raise ValueError("amounts must be a list of the same length as hotkey_ss58s") + + if netuids is not None and len(netuids) != len(hotkey_ss58s): + raise ValueError("netuids must be a list of the same length as hotkey_ss58s") + + logging.info( + f":satellite: [magenta]Syncing with chain:[/magenta] [blue]{subtensor.network}[/blue] [magenta]...[/magenta]" + ) + block = subtensor.get_current_block() + old_balance = subtensor.get_balance(wallet.coldkeypub.ss58_address, block=block) + all_stakes = subtensor.get_stake_for_coldkey( + coldkey_ss58=wallet.coldkeypub.ss58_address + ) + old_stakes = get_old_stakes( + wallet=wallet, hotkey_ss58s=hotkey_ss58s, netuids=netuids, all_stakes=all_stakes + ) + + successful_unstakes = 0 + for idx, (hotkey_ss58, amount, old_stake, netuid) in enumerate( + zip(hotkey_ss58s, amounts, old_stakes, netuids) + ): + # Convert to bittensor.Balance + if amount is None: + # Unstake it all. + unstaking_balance = old_stake + logging.warning( + f"Didn't receive any unstaking amount. Unstaking all existing stake: [blue]{old_stake}[/blue] " + f"from hotkey: [blue]{hotkey_ss58}[/blue]" + ) + else: + unstaking_balance = amount + unstaking_balance.set_unit(netuid) + + # Check enough to unstake. + if unstaking_balance > old_stake: + logging.error( + f":cross_mark: [red]Not enough stake[/red]: [green]{old_stake}[/green] to unstake: " + f"[blue]{unstaking_balance}[/blue] from hotkey: [blue]{wallet.hotkey_str}[/blue]." + ) + continue + + try: + call = subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="remove_stake", + call_params={ + "hotkey": hotkey_ss58, + "amount_unstaked": unstaking_balance.rao, + "netuid": netuid, + }, + ) + fee = get_extrinsic_fee( + subtensor=subtensor, netuid=netuid, call=call, keypair=wallet.coldkeypub + ) + logging.info( + f"Unstaking [blue]{unstaking_balance}[/blue] from hotkey: [magenta]{hotkey_ss58}[/magenta] on netuid: " + f"[blue]{netuid}[/blue] for fee [blue]{fee}[/blue]" + ) + + success, message = subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + nonce_key="coldkeypub", + sign_with="coldkey", + use_nonce=True, + period=period, + raise_error=raise_error, + ) + + if success: # If we successfully unstaked. + # We only wait here if we expect finalization. + + if not wait_for_finalization and not wait_for_inclusion: + successful_unstakes += 1 + continue + + logging.info(":white_heavy_check_mark: [green]Finalized[/green]") + + logging.info( + f":satellite: [magenta]Checking Balance on:[/magenta] [blue]{subtensor.network}[/blue] " + f"[magenta]...[/magenta]..." + ) + new_stake = subtensor.get_stake( + coldkey_ss58=wallet.coldkeypub.ss58_address, + hotkey_ss58=hotkey_ss58, + netuid=netuid, + block=block, + ) + logging.info( + f"Stake ({hotkey_ss58}) on netuid {netuid}: [blue]{old_stake}[/blue] :arrow_right: [green]{new_stake}[/green]" + ) + successful_unstakes += 1 + else: + logging.error(f":cross_mark: [red]Failed: {message}.[/red]") + continue + + except SubstrateRequestException as error: + logging.error( + f":cross_mark: [red]Multiple unstake filed with error: {format_error_message(error)}[/red]" + ) + return False + + if successful_unstakes != 0: + logging.info( + f":satellite: [magenta]Checking Balance on:[/magenta] ([blue]{subtensor.network}[/blue] " + f"[magenta]...[/magenta]" + ) + new_balance = subtensor.get_balance(wallet.coldkeypub.ss58_address) + logging.info( + f"Balance: [blue]{old_balance}[/blue] :arrow_right: [green]{new_balance}[/green]" + ) + return True + + return False diff --git a/bittensor/core/extrinsics/utils.py b/bittensor/core/extrinsics/utils.py new file mode 100644 index 0000000000..5092791f66 --- /dev/null +++ b/bittensor/core/extrinsics/utils.py @@ -0,0 +1,70 @@ +"""Module with helper functions for extrinsics.""" + +from typing import TYPE_CHECKING, Optional + +from bittensor.utils.balance import Balance + +if TYPE_CHECKING: + from scalecodec import GenericCall + from bittensor_wallet import Wallet, Keypair + from bittensor.core.chain_data import StakeInfo + from bittensor.core.subtensor import Subtensor + + +def get_old_stakes( + wallet: "Wallet", + hotkey_ss58s: list[str], + netuids: list[int], + all_stakes: list["StakeInfo"], +) -> list["Balance"]: + """ + Retrieve the previous staking balances for a wallet's hotkeys across given netuids. + + This function searches through the provided staking data to find the stake amounts for the specified hotkeys and + netuids associated with the wallet's coldkey. If no match is found for a particular hotkey and netuid combination, + a default balance of zero is returned. + + Args: + wallet: The wallet containing the coldkey to compare with stake data. + hotkey_ss58s: List of hotkey SS58 addresses for which stakes are retrieved. + netuids: List of network unique identifiers (netuids) corresponding to the hotkeys. + all_stakes: A collection of all staking information to search through. + + Returns: + list[Balance]: A list of Balances, each representing the stake for a given hotkey and netuid. + """ + stake_lookup = { + (stake.hotkey_ss58, stake.coldkey_ss58, stake.netuid): stake.stake + for stake in all_stakes + } + return [ + stake_lookup.get( + (hotkey_ss58, wallet.coldkeypub.ss58_address, netuid), + Balance.from_tao(0), # Default to 0 balance if no match found + ) + for hotkey_ss58, netuid in zip(hotkey_ss58s, netuids) + ] + + +def get_extrinsic_fee( + call: "GenericCall", + keypair: "Keypair", + subtensor: "Subtensor", + netuid: Optional[int] = None, +): + """ + Get extrinsic fee for a given extrinsic call and keypair for a given SN's netuid. + + Arguments: + subtensor: The Subtensor instance. + call: The extrinsic call. + keypair: The keypair associated with the extrinsic. + netuid: The SN's netuid. + + Returns: + Balance object representing the extrinsic fee in RAO. + """ + payment_info = subtensor.substrate.get_payment_info(call=call, keypair=keypair) + return Balance.from_rao(amount=payment_info["partial_fee"]).set_unit( + netuid=netuid or 0 + ) diff --git a/bittensor/core/extrinsics/weights.py b/bittensor/core/extrinsics/weights.py new file mode 100644 index 0000000000..0a3aa8e264 --- /dev/null +++ b/bittensor/core/extrinsics/weights.py @@ -0,0 +1,228 @@ +"""Module sync commit weights and reveal weights extrinsic.""" + +from typing import TYPE_CHECKING, Optional, Union + +import numpy as np +from numpy.typing import NDArray + +from bittensor.core.settings import version_as_int +from bittensor.utils import get_function_name +from bittensor.utils.btlogging import logging +from bittensor.utils.weight_utils import ( + convert_and_normalize_weights_and_uids, + convert_uids_and_weights, +) + +if TYPE_CHECKING: + from bittensor.core.subtensor import Subtensor + from bittensor_wallet import Wallet + from bittensor.utils.registration import torch + + +def commit_weights_extrinsic( + subtensor: "Subtensor", + wallet: "Wallet", + netuid: int, + commit_hash: str, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = False, + wait_for_finalization: bool = False, +) -> tuple[bool, str]: + """ + Commits a hash of the neuron's weights to the Bittensor blockchain using the provided wallet. + This function is a wrapper around the `do_commit_weights` method. + + Parameters: + subtensor: The subtensor instance used for blockchain interaction. + wallet: The wallet associated with the neuron committing the weights. + netuid: The unique identifier of the subnet. + commit_hash: The hash of the neuron's weights to be committed. + 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: Whether to raise an error if the transaction fails. + 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. + + Returns: + tuple[bool, str]: + `True` if the weight commitment is successful, `False` otherwise. + `msg` is a string value describing the success or potential error. + + This function provides a user-friendly interface for committing weights to the Bittensor blockchain, ensuring proper + error handling and user interaction when required. + """ + + call = subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="commit_weights", + call_params={ + "netuid": netuid, + "commit_hash": commit_hash, + }, + ) + success, message = subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + use_nonce=True, + period=period, + sign_with="hotkey", + nonce_key="hotkey", + raise_error=raise_error, + ) + + if success: + logging.info(message) + else: + logging.error(f"{get_function_name()}: {message}") + return success, message + + +def reveal_weights_extrinsic( + subtensor: "Subtensor", + wallet: "Wallet", + netuid: int, + uids: list[int], + weights: list[int], + salt: list[int], + version_key: int, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = False, + wait_for_finalization: bool = False, +) -> tuple[bool, str]: + """ + Reveals the weights for a specific subnet on the Bittensor blockchain using the provided wallet. + This function is a wrapper around the `_do_reveal_weights` method. + + Parameters: + subtensor: The subtensor instance used for blockchain interaction. + wallet: The wallet associated with the neuron revealing the weights. + netuid: The unique identifier of the subnet. + uids: List of neuron UIDs for which weights are being revealed. + weights: List of weight values corresponding to each UID. + salt: List of salt values corresponding to the hash function. + version_key: Version key for compatibility with the network. + 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: Whether to raise an error if the transaction fails. + 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. + + Returns: + tuple[bool, str]: + `True` if the weight commitment is successful, `False` otherwise. + `msg` is a string value describing the success or potential error. + + This function provides a user-friendly interface for revealing weights on the Bittensor blockchain, ensuring proper + error handling and user interaction when required. + """ + + call = subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="reveal_weights", + call_params={ + "netuid": netuid, + "uids": uids, + "values": weights, + "salt": salt, + "version_key": version_key, + }, + ) + + success, message = subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + use_nonce=True, + period=period, + sign_with="hotkey", + nonce_key="hotkey", + raise_error=raise_error, + ) + + if success: + logging.info(message) + else: + logging.error(f"{get_function_name()}: {message}") + return success, message + + +def set_weights_extrinsic( + subtensor: "Subtensor", + wallet: "Wallet", + netuid: int, + uids: Union[NDArray[np.int64], "torch.LongTensor", list], + weights: Union[NDArray[np.float32], "torch.FloatTensor", list], + version_key: int = version_as_int, + period: Optional[int] = 8, + raise_error: bool = False, + wait_for_inclusion: bool = False, + wait_for_finalization: bool = False, +) -> tuple[bool, str]: + """ + Sets the given weights and values on a chain for a wallet hotkey account. + + Parameters: + subtensor: Bittensor subtensor object. + wallet: Bittensor wallet object. + netuid: The ``netuid`` of the subnet to set weights for. + uids: The ``uint64`` uids of destination neurons. + weights: The weights to set. These must be ``float``s and correspond to the passed ``uid``s. + version_key: The version key of the validator. + 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: Whether to raise an error if the transaction fails. + 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. + + Returns: + tuple[bool, str]: + `True` if the weight commitment is successful, `False` otherwise. + `msg` is a string value describing the success or potential error. + """ + # Convert types. + uids, weights = convert_uids_and_weights(uids, weights) + + # Reformat and normalize. + weight_uids, weight_vals = convert_and_normalize_weights_and_uids(uids, weights) + + logging.info( + f":satellite: [magenta]Setting weights on [/magenta]" + f"[blue]{subtensor.network}[/blue] " + f"[magenta]...[/magenta]" + ) + + call = subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="set_weights", + call_params={ + "dests": weight_uids, + "weights": weight_vals, + "netuid": netuid, + "version_key": version_key, + }, + ) + success, message = subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + period=period, + use_nonce=True, + nonce_key="hotkey", + sign_with="hotkey", + raise_error=raise_error, + ) + + if success: + logging.info("Successfully set weights and Finalized.") + else: + logging.error(f"{get_function_name}: {message}") + return success, message diff --git a/bittensor/core/metagraph.py b/bittensor/core/metagraph.py new file mode 100644 index 0000000000..af758939ea --- /dev/null +++ b/bittensor/core/metagraph.py @@ -0,0 +1,1972 @@ +import asyncio +import contextlib +import copy +import os +import pickle +import typing +from abc import ABC, abstractmethod +from os import listdir +from os.path import join +from typing import Optional, Union + +import numpy as np +from async_substrate_interface.errors import SubstrateRequestException +from numpy.typing import NDArray +from packaging import version + +from bittensor.core import settings +from bittensor.core.chain_data import ( + AxonInfo, + SubnetState, + MetagraphInfoEmissions, + MetagraphInfoPool, + MetagraphInfoParams, +) +from bittensor.utils import determine_chain_endpoint_and_network +from bittensor.utils.btlogging import logging +from bittensor.utils.registration import torch, use_torch +from bittensor.utils.weight_utils import ( + convert_weight_uids_and_vals_to_tensor, + convert_bond_uids_and_vals_to_tensor, + convert_root_weight_uids_and_vals_to_tensor, +) + +# For annotation purposes +if typing.TYPE_CHECKING: + from bittensor.core.subtensor import Subtensor + from bittensor.core.async_subtensor import AsyncSubtensor + from bittensor.core.chain_data import ( + ChainIdentity, + MetagraphInfo, + NeuronInfo, + NeuronInfoLite, + SubnetIdentity, + ) + + +Tensor = Union["torch.nn.Parameter", NDArray] + + +METAGRAPH_STATE_DICT_NDARRAY_KEYS = [ + "version", + "n", + "block", + "stake", + "ranks", + "trust", + "consensus", + "validator_trust", + "incentive", + "emission", + "dividends", + "active", + "last_update", + "validator_permit", + "uids", + "netuid", + "weights", + "axons", + "neurons", + "bonds", +] +"""List of keys for the metagraph state dictionary used in NDArray serialization. + +This list defines the set of keys expected in the metagraph's state dictionary when serializing and deserializing NumPy +ndarray objects. Each key corresponds to a specific attribute or metric associated with the nodes in the metagraph. + +- **version** (`str`): The version identifier of the metagraph state. +- **n** (`int`): The total number of nodes in the metagraph. +- **block** (`int`): The current block number in the blockchain or ledger. +- **stake** (`ndarray`): An array representing the stake of each node. +- **total_stake** (`float`): The sum of all individual stakes in the metagraph. +- **ranks** (`ndarray`): An array of rank scores assigned to each node. +- **trust** (`ndarray`): An array of trust scores for the nodes. +- **consensus** (`ndarray`): An array indicating consensus levels among nodes. +- **validator_trust** (`ndarray`): Trust scores specific to validator nodes. +- **incentive** (`ndarray`): Incentive values allocated to nodes. +- **emission** (`float`): The rate of emission for new tokens or units. +- **dividends** (`ndarray`): Dividend amounts distributed to nodes. +- **active** (`ndarray`): Boolean array indicating active (`True`) or inactive (`False`) nodes. +- **last_update** (`int`): Timestamp of the last state update. +- **validator_permit** (`ndarray`): Boolean array indicating nodes permitted to validate. +- **uids** (`ndarray`): Unique identifiers for each node in the metagraph. +""" + + +def get_save_dir( + network: str, netuid: int, root_dir: Optional[list[str]] = None +) -> str: + """ + Returns a directory path given ``network`` and ``netuid`` inputs. + + Args: + network (str): Network name. + netuid (int): Network UID. + root_dir: list to the file path for the root directory of your metagraph saves (i.e. ['/', 'tmp', 'metagraphs'], + defaults to ["~", ".bittensor", "metagraphs"] + + Returns: + str: Directory path. + """ + _root_dir = root_dir or ["~", ".bittensor", "metagraphs"] + return os.path.expanduser( + os.path.join( + *_root_dir, + f"network-{str(network)}", + f"netuid-{str(netuid)}", + ) + ) + + +def latest_block_path(dir_path: str) -> str: + """ + Get the latest block path from the provided directory path. + + Args: + dir_path (str): Directory path. + + Returns: + str: Latest block path. + """ + latest_block = -1 + latest_file_full_path = None + for filename in listdir(dir_path): + full_path_filename = os.path.expanduser(join(dir_path, filename)) + try: + block_number = int(filename.split("-")[1].split(".")[0]) + if block_number > latest_block: + latest_block = block_number + latest_file_full_path = full_path_filename + except Exception: + pass + if not latest_file_full_path: + raise ValueError(f"Metagraph not found at: {dir_path}") + else: + return latest_file_full_path + + +def safe_globals(): + """ + Context manager to load torch files for version 2.6+ + """ + if version.parse(torch.__version__).release < version.parse("2.6").release: + return contextlib.nullcontext() + + np_core = ( + np._core if version.parse(np.__version__) >= version.parse("2.0.0") else np.core + ) + allow_list = [ + np_core.multiarray._reconstruct, + np.ndarray, + np.dtype, + type(np.dtype(np.uint32)), + np.dtypes.Float32DType, + bytes, + ] + return torch.serialization.safe_globals(allow_list) + + +class MetagraphMixin(ABC): + """ + The metagraph class is a core component of the Bittensor network, representing the neural graph that forms the + backbone of the decentralized machine learning system. + + The metagraph is a dynamic representation of the network's state, capturing the interconnectedness and attributes of + neurons (participants) in the Bittensor ecosystem. This class is not just a static structure but a live + reflection of the network, constantly updated and synchronized with the state of the blockchain. + + In Bittensor, neurons are akin to nodes in a distributed system, each contributing computational resources and + participating in the network's collective intelligence. The metagraph tracks various attributes of these + neurons, such as stake, trust, and consensus, which are crucial for the network's incentive mechanisms and the + Yuma Consensus algorithm as outlined in the `NeurIPS paper + `_. These attributes govern how neurons + interact, how they are incentivized, and their roles within the network's decision-making processes. + + Args: + netuid (int): A unique identifier that distinguishes between different instances or versions of the Bittensor network. + network (str): The name of the network, signifying specific configurations or iterations within the Bittensor ecosystem. + + Attributes: + version (NDArray): The version number of the network, integral for tracking network updates. + n (NDArray): The total number of neurons in the network, reflecting its size and complexity. + block (NDArray): The current block number in the blockchain, crucial for synchronizing with the network's latest state. + stake: Represents the cryptocurrency staked by neurons, impacting their influence and earnings within the network. + total_stake: The cumulative stake across all neurons. + ranks: Neuron rankings as per the Yuma Consensus algorithm, influencing their incentive distribution and network authority. + trust: Scores indicating the reliability of neurons, mainly miners, within the network's operational context. + consensus: Scores reflecting each neuron's alignment with the network's collective decisions. + validator_trust: Trust scores for validator neurons, crucial for network security and validation. + incentive: Rewards allocated to neurons, particularly miners, for their network contributions. + emission: The rate at which rewards are distributed to neurons. + dividends: Rewards received primarily by validators as part of the incentive mechanism. + active: Status indicating whether a neuron is actively participating in the network. + last_update: Timestamp of the latest update to a neuron's data. + validator_permit: Indicates if a neuron is authorized to act as a validator. + weights: Inter-neuronal weights set by each neuron, influencing network dynamics. + bonds: Represents speculative investments by neurons in others, part of the reward mechanism. + uids: Unique identifiers for each neuron, essential for network operations. + axons (List): Details about each neuron's axon, critical for facilitating network communication. + + The metagraph plays a pivotal role in Bittensor's decentralized AI operations, influencing everything from data + propagation to reward distribution. It embodies the principles of decentralized governance and collaborative + intelligence, ensuring that the network remains adaptive, secure, and efficient. + + Example: + Initializing the metagraph to represent the current state of the Bittensor network:: + + from bittensor.core.metagraph import Metagraph + metagraph = Metagraph(netuid=config.netuid, network=subtensor.network, sync=False) + + Synchronizing the metagraph with the network to reflect the latest state and neuron data:: + + metagraph.sync(subtensor=subtensor) + + Accessing metagraph properties to inform network interactions and decisions:: + + total_stake = metagraph.S + neuron_ranks = metagraph.R + neuron_incentives = metagraph.I + axons = metagraph.axons + neurons = metagraph.neurons + + Maintaining a local copy of hotkeys for querying and interacting with network entities:: + + hotkeys = deepcopy(metagraph.hotkeys) + """ + + netuid: int + network: str + version: Union["torch.nn.Parameter", tuple[NDArray]] + n: Tensor + neurons: list[Union["NeuronInfo", "NeuronInfoLite"]] + block: Tensor + ranks: Tensor + trust: Tensor + consensus: Tensor + validator_trust: Tensor + incentive: Tensor + emission: Tensor + dividends: Tensor + active: Tensor + last_update: Tensor + validator_permit: Tensor + weights: Tensor + bonds: Tensor + uids: Tensor + alpha_stake: Tensor + tao_stake: Tensor + stake: Tensor + axons: list[AxonInfo] + chain_endpoint: Optional[str] + subtensor: Optional[Union["AsyncSubtensor", "Subtensor"]] + _dtype_registry = {"int64": np.int64, "float32": np.float32, "bool": bool} + + # metagraph_info fields + name: str + symbol: str + network_registered_at: int + num_uids: int + max_uids: int + identities: list[Optional["ChainIdentity"]] + identity: Optional["SubnetIdentity"] + pruning_score: list[float] + block_at_registration: list[int] + tao_dividends_per_hotkey: list[tuple[str, float]] + alpha_dividends_per_hotkey: list[tuple[str, float]] + last_step: int + tempo: int + blocks_since_last_step: int + owner_coldkey: str + owner_hotkey: str + + hparams: MetagraphInfoParams + pool: MetagraphInfoPool + emissions: MetagraphInfoEmissions + + @property + def TS(self) -> Tensor: + """ + Represents the tao stake of each neuron in the Bittensor network. + + Returns: + Tensor: The list of tao stake of each neuron in the network. + """ + return self.tao_stake + + @property + def AS(self) -> Tensor: + """ + Represents the alpha stake of each neuron in the Bittensor network. + + Returns: + Tensor: The list of alpha stake of each neuron in the network. + """ + return self.alpha_stake + + @property + def S(self) -> Tensor: + """ + Represents the stake of each neuron in the Bittensor network. Stake is an important concept in the + Bittensor ecosystem, signifying the amount of network weight (or “stake”) each neuron holds, + represented on a digital ledger. The stake influences a neuron's ability to contribute to and benefit + from the network, playing a crucial role in the distribution of incentives and decision-making processes. + + Returns: + Tensor: A tensor representing the stake of each neuron in the network. Higher values signify a greater + stake held by the respective neuron. + """ + return self.stake + + @property + def R(self) -> Tensor: + """ + Contains the ranks of neurons in the Bittensor network. Ranks are determined by the network based + on each neuron's performance and contributions. Higher ranks typically indicate a greater level of + contribution or performance by a neuron. These ranks are crucial in determining the distribution of + incentives within the network, with higher-ranked neurons receiving more incentive. + + Returns: + Tensor: A tensor where each element represents the rank of a neuron. Higher values indicate higher ranks + within the network. + """ + return self.ranks + + @property + def I(self) -> Tensor: + """ + 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 + informational value, stake, and consensus with other peers. This ensures that the most valuable and + trusted contributions are incentivized. + + Returns: + Tensor: A tensor of incentive values, indicating the rewards or benefits accrued by each neuron based on + their contributions and network consensus. + """ + return self.incentive + + @property + def E(self) -> Tensor: + """ + Denotes the emission values of neurons in the Bittensor network. Emissions refer to the distribution or + release of rewards (often in the form of cryptocurrency) to neurons, typically based on their stake and + performance. This mechanism is central to the network's incentive model, ensuring that active and + contributing neurons are appropriately rewarded. + + Returns: + Tensor: A tensor where each element represents the emission value for a neuron, indicating the amount of + reward distributed to that neuron. + """ + return self.emission + + @property + def C(self) -> Tensor: + """ + Represents the consensus values of neurons in the Bittensor network. Consensus is a measure of how + much a neuron's contributions are trusted and agreed upon by the majority of the network. It is + calculated based on a staked weighted trust system, where the network leverages the collective + judgment of all participating peers. Higher consensus values indicate that a neuron's contributions + are more widely trusted and valued across the network. + + Returns: + Tensor: A tensor of consensus values, where each element reflects the level of trust and agreement a neuron + has achieved within the network. + + """ + return self.consensus + + @property + def T(self) -> Tensor: + """ + Represents the trust values assigned to each neuron in the Bittensor network. Trust is a key metric that + reflects the reliability and reputation of a neuron based on its past behavior and contributions. It is + an essential aspect of the network's functioning, influencing decision-making processes and interactions + between neurons. + + The trust matrix is inferred from the network's inter-peer weights, indicating the level of trust each neuron + has in others. A higher value in the trust matrix suggests a stronger trust relationship between neurons. + + Returns: + Tensor: A tensor of trust values, where each element represents the trust level of a neuron. Higher values + denote a higher level of trust within the network. + """ + return self.trust + + @property + def Tv(self) -> Tensor: + """ + Contains the validator trust values of neurons in the Bittensor network. Validator trust is specifically + associated with neurons that act as validators within the network. This specialized form of trust reflects + the validators' reliability and integrity in their role, which is crucial for maintaining the network's + stability and security. + + Validator trust values are particularly important for the network's consensus and validation processes, + determining the validators' influence and responsibilities in these critical functions. + + Returns: + Tensor: A tensor of validator trust values, specifically applicable to neurons serving as validators, where + higher values denote greater trustworthiness in their validation roles. + """ + return self.validator_trust + + @property + def D(self) -> Tensor: + """ + Represents the dividends received by neurons in the Bittensor network. Dividends are a form of reward or + distribution, typically given to neurons based on their stake, performance, and contribution to the network. + They are an integral part of the network's incentive structure, encouraging active and beneficial participation. + + Returns: + Tensor: A tensor of dividend values, where each element indicates the dividends received by a neuron, + reflecting their share of network rewards. + """ + return self.dividends + + @property + def B(self) -> Tensor: + """ + Bonds in the Bittensor network represent a speculative reward mechanism where neurons can accumulate + bonds in other neurons. Bonds are akin to investments or stakes in other neurons, reflecting a belief in + their future value or performance. This mechanism encourages correct weighting and collaboration + among neurons while providing an additional layer of incentive. + + Returns: + Tensor: A tensor representing the bonds held by each neuron, where each value signifies the proportion of + bonds owned by one neuron in another. + """ + return self.bonds + + @property + def W(self) -> Tensor: + """ + Represents the weights assigned to each neuron in the Bittensor network. In the context of Bittensor, + weights are crucial for determining the influence and interaction between neurons. Each neuron is responsible + for setting its weights, which are then recorded on a digital ledger. These weights are reflective of the + neuron's assessment or judgment of other neurons in the network. + + The weight matrix :math:`W = [w_{ij}]` is a key component of the network's architecture, where the :math: + `i^{th}` row is set by neuron :math:`i` and represents its weights towards other neurons. These weights + influence the ranking and incentive mechanisms within the network. Higher weights from a neuron towards another + can imply greater trust or value placed on that neuron's contributions. + + Returns: + Tensor: A tensor of inter-peer weights, where each element :math:`w_{ij}` represents the weight assigned by + neuron :math:`i` to neuron :math:`j`. This matrix is fundamental to the network's functioning, + influencing the distribution of incentives and the inter-neuronal dynamics. + """ + return self.weights + + @property + def hotkeys(self) -> list[str]: + """ + Represents a list of ``hotkeys`` for each neuron in the Bittensor network. + + Hotkeys are unique identifiers used by neurons for active participation in the network, such as sending and + receiving information or transactions. They are akin to public keys in cryptographic systems and are essential + for identifying and authenticating neurons within the network's operations. + + Returns: + List[str]: A list of hotkeys, with each string representing the hotkey of a corresponding neuron. + + These keys are crucial for the network's security and integrity, ensuring proper identification and + authorization of network participants. + + Note: + While the `NeurIPS paper `_ may not + explicitly detail the concept of hotkeys, they are a fundamental of decentralized networks for secure + and authenticated interactions. + """ + return [axon.hotkey for axon in self.axons] + + @property + def coldkeys(self) -> list[str]: + """ + Contains a list of ``coldkeys`` for each neuron in the Bittensor network. + + Coldkeys are similar to hotkeys but are typically used for more secure, offline activities such as storing + assets or offline signing of transactions. They are an important aspect of a neuron's security, providing an + additional layer of protection for sensitive operations and assets. + + Returns: + List[str]: A list of coldkeys, each string representing the coldkey of a neuron. These keys play a vital + role in the secure management of assets and sensitive operations within the network. + + Note: + The concept of coldkeys, while not explicitly covered in the NeurIPS paper, is a standard practice in + blockchain and decentralized networks for enhanced security and asset protection. + """ + return [axon.coldkey for axon in self.axons] + + @property + def addresses(self) -> list[str]: + """ + Provides a list of IP addresses for each neuron in the Bittensor network. These addresses are used for + network communication, allowing neurons to connect, interact, and exchange information with each other. + IP addresses are fundamental for the network's peer-to-peer communication infrastructure. + + Returns: + List[str]: A list of IP addresses, with each string representing the address of a neuron. These addresses + enable the decentralized, distributed nature of the network, facilitating direct communication and data + exchange among neurons. + + Note: + While IP addresses are a basic aspect of network communication, specific details about their use in + the Bittensor network may not be covered in the `NeurIPS paper + `_. They are, however, integral to + the functioning of any distributed network. + """ + return [axon.ip_str() for axon in self.axons] + + @abstractmethod + def __init__( + self, + netuid: int, + network: str = settings.DEFAULT_NETWORK, + lite: bool = True, + sync: bool = True, + subtensor: Optional[Union["AsyncSubtensor", "Subtensor"]] = None, + ): + """ + Initializes a new instance of the metagraph object, setting up the basic structure and parameters based on the + provided arguments. This method is the entry point for creating a metagraph object, which is a central component + in representing the state of the Bittensor network. + + Args: + netuid (int): The unique identifier for the network, distinguishing this instance of the metagraph within + potentially multiple network configurations. + network (str): The name of the network, which can indicate specific configurations or versions of the + Bittensor network. + lite (bool): A flag indicating whether to use a lite version of the metagraph. The lite version may contain + less detailed information but can be quicker to initialize and sync. + sync (bool): A flag indicating whether to synchronize the metagraph with the network upon initialization. + Synchronization involves updating the metagraph's parameters to reflect the current state of the network. + + Example: + Initializing a metagraph object for the Bittensor network with a specific network UID:: + + metagraph = Metagraph(netuid=123, network="finney", lite=True, sync=True) + """ + self.lite = lite + self.subtensor = subtensor + self.should_sync = sync + self.netuid = netuid + self.network, self.chain_endpoint = determine_chain_endpoint_and_network( + network + ) + self.neurons = [] + self.axons: list[AxonInfo] = [] + + def __str__(self) -> str: + """ + Provides a human-readable string representation of the metagraph object. This representation includes key + identifiers and attributes of the metagraph, making it easier to quickly understand the state and configuration + of the metagraph in a simple format. + + Returns: + str: A string that succinctly represents the metagraph, including its network UID, the total number of + neurons (n), the current block number, and the network's name. This format is particularly useful + for logging, debugging, and displaying the metagraph in a concise manner. + + Example: + When printing the metagraph object or using it in a string context, this method is automatically invoked:: + + print(metagraph) # Output: "metagraph(netuid:1, n:100, block:500, network:finney)" + """ + return f"metagraph(netuid:{self.netuid}, n:{self.n.item()}, block:{self.block.item()}, network:{self.network})" + + def __repr__(self) -> str: + """ + Provides a detailed string representation of the metagraph object, intended for unambiguous understanding and + debugging purposes. This method simply calls the :func:`__str__` method, ensuring consistency between the + informal and formal string representations of the metagraph. + + Returns: + str: The same string representation as provided by the :func:`__str__` method, detailing the metagraph's key + attributes including network UID, number of neurons, block number, and network name. + + Example: + The :func:`__repr__` output can be used in debugging to get a clear and concise description of the metagraph:: + + metagraph_repr = repr(metagraph) + print(metagraph_repr) # Output mirrors that of __str__ + """ + return self.__str__() + + def metadata(self) -> dict: + """ + Retrieves the metadata of the metagraph, providing key information about the current state of the Bittensor + network. This metadata includes details such as the network's unique identifier (``netuid``), the total number + of neurons (``n``), the current block number, the network's name, and the version of the Bittensor network. + + Returns: + dict: A dictionary containing essential metadata about the metagraph, including: + + - ``netuid``: The unique identifier for the network. + - ``n``: The total number of neurons in the network. + - ``block``: The current block number in the network's blockchain. + - ``network``: The name of the Bittensor network. + - ``version``: The version number of the Bittensor software. + + Note: + This metadata is crucial for understanding the current state and configuration of the network, as well as + for tracking its evolution over time. + """ + return { + "netuid": self.netuid, + "n": self.n.item(), + "block": self.block.item(), + "network": self.network, + "version": settings.__version__, + } + + def state_dict(self): + return { + "netuid": self.netuid, + "network": self.network, + "version": self.version, + "n": self.n, + "block": self.block, + "ranks": self.ranks, + "trust": self.trust, + "consensus": self.consensus, + "validator_trust": self.validator_trust, + "incentive": self.incentive, + "emission": self.emission, + "dividends": self.dividends, + "active": self.active, + "last_update": self.last_update, + "validator_permit": self.validator_permit, + "weights": self.weights, + "bonds": self.bonds, + "uids": self.uids, + "axons": self.axons, + "neurons": self.neurons, + "alpha_stake": self.alpha_stake, + "tao_stake": self.tao_stake, + "stake": self.stake, + } + + @staticmethod + def _create_tensor(data, dtype) -> Tensor: + """ + Creates a numpy array with the given data and data type. This method is a utility function used internally to + encapsulate data into a np.array, making it compatible with the metagraph's numpy model structure. + + Args: + data: The data to be included in the tensor. This could be any numeric data, like stakes, ranks, etc. + dtype: The data type for the tensor, typically a numpy data type like ``np.float32`` or ``np.int64``. + + Returns: + Tensor: A tensor parameter encapsulating the provided data. + + Internal Usage: + Used internally to create tensor parameters for various metagraph attributes:: + + self.stake = self._create_tensor(neuron_stakes, dtype=np.float32) + """ + # TODO: Check and test the creation of tensor + return ( + torch.nn.Parameter(torch.tensor(data, dtype=dtype), requires_grad=False) + if use_torch() + else np.array(data, dtype=dtype) + ) + + def _process_weights_or_bonds(self, data, attribute: str) -> Tensor: + """ + Processes the raw weights or bonds data and converts it into a structured tensor format. This method handles the + transformation of neuron connection data (``weights`` or ``bonds``) from a list or other unstructured format + into a tensor that can be utilized within the metagraph model. + + Args: + data: The raw weights or bonds data to be processed. This data typically comes from the subtensor. + attribute: A string indicating whether the data is ``weights`` or ``bonds``, which determines the specific + processing steps to be applied. + + Returns: + Tensor: A tensor parameter encapsulating the processed weights or bonds data. + + Internal Usage: + Used internally to process and set weights or bonds for the neurons:: + + self.weights = self._process_weights_or_bonds(raw_weights_data, "weights") + """ + data_array = [] + for item in data: + if len(item) == 0: + if use_torch(): + data_array.append(torch.zeros(len(self.neurons))) + else: + data_array.append(np.zeros(len(self.neurons), dtype=np.float32)) + else: + uids, values = zip(*item) + # TODO: Validate and test the conversion of uids and values to tensor + if attribute == "weights": + data_array.append( + convert_weight_uids_and_vals_to_tensor( + len(self.neurons), + list(uids), + list(values), + ) + ) + else: + data_array.append( + convert_bond_uids_and_vals_to_tensor( + len(self.neurons), list(uids), list(values) + ).astype(np.float32) + ) + tensor_param: Tensor = ( + ( + torch.nn.Parameter(torch.stack(data_array), requires_grad=False) + if len(data_array) + else torch.nn.Parameter() + ) + if use_torch() + else ( + np.stack(data_array) + if len(data_array) + else np.array([], dtype=np.float32) + ) + ) + if len(data_array) == 0: + logging.warning( + f"Empty {attribute}_array on metagraph.sync(). The '{attribute}' tensor is empty." + ) + return tensor_param + + def _set_metagraph_attributes(self, block: int): + """ + Sets various attributes of the metagraph based on the latest network data fetched from the subtensor. This + method updates parameters like the number of neurons, block number, stakes, trusts, ranks, and other + neuron-specific information. + + Args: + block (int): The block number for which the metagraph attributes need to be set. + + Internal Usage: + Used internally during the sync process to update the metagraph's attributes:: + + self._set_metagraph_attributes(block) + """ + self.n = self._create_tensor( + len(self.neurons), dtype=self._dtype_registry["int64"] + ) + self.version = self._create_tensor( + [settings.version_as_int], dtype=self._dtype_registry["int64"] + ) + self.block = self._create_tensor(block, dtype=self._dtype_registry["int64"]) + self.uids = self._create_tensor( + [neuron.uid for neuron in self.neurons], dtype=self._dtype_registry["int64"] + ) + self.trust = self._create_tensor( + [neuron.trust for neuron in self.neurons], + dtype=self._dtype_registry["float32"], + ) + self.consensus = self._create_tensor( + [neuron.consensus for neuron in self.neurons], + dtype=self._dtype_registry["float32"], + ) + self.incentive = self._create_tensor( + [neuron.incentive for neuron in self.neurons], + dtype=self._dtype_registry["float32"], + ) + self.dividends = self._create_tensor( + [neuron.dividends for neuron in self.neurons], + dtype=self._dtype_registry["float32"], + ) + self.ranks = self._create_tensor( + [neuron.rank for neuron in self.neurons], + dtype=self._dtype_registry["float32"], + ) + self.emission = self._create_tensor( + [neuron.emission for neuron in self.neurons], + dtype=self._dtype_registry["float32"], + ) + self.active = self._create_tensor( + [neuron.active for neuron in self.neurons], + dtype=self._dtype_registry["int64"], + ) + self.last_update = self._create_tensor( + [neuron.last_update for neuron in self.neurons], + dtype=self._dtype_registry["int64"], + ) + self.validator_permit = self._create_tensor( + [neuron.validator_permit for neuron in self.neurons], dtype=bool + ) + self.validator_trust = self._create_tensor( + [neuron.validator_trust for neuron in self.neurons], + dtype=self._dtype_registry["float32"], + ) + self.axons = [n.axon_info for n in self.neurons] + + def save(self, root_dir: Optional[list[str]] = None) -> "MetagraphMixin": + """ + Saves the current state of the metagraph to a file on disk. This function is crucial for persisting the current + state of the network's metagraph, which can later be reloaded or analyzed. The save operation includes all + neuron attributes and parameters, ensuring a complete snapshot of the metagraph's state. + + Args: + root_dir: list to the file path for the root directory of your metagraph saves + (i.e. ['/', 'tmp', 'metagraphs'], defaults to ["~", ".bittensor", "metagraphs"] + + Returns: + metagraph (bittensor.core.metagraph.Metagraph): The metagraph instance after saving its state. + + Example: + Save the current state of the metagraph to the default directory:: + + metagraph.save() + + The saved state can later be loaded to restore or analyze the metagraph's state at this point. + + If using the default save path:: + + metagraph.load() + + If using a custom save path:: + + metagraph.load_from_path(dir_path) + """ + save_directory = get_save_dir(self.network, self.netuid, root_dir=root_dir) + os.makedirs(save_directory, exist_ok=True) + if use_torch(): + graph_filename = f"{save_directory}/block-{self.block.item()}.pt" + state_dict = self.state_dict() + state_dict["axons"] = self.axons + state_dict["neurons"] = self.neurons + torch.save(state_dict, graph_filename) + torch.load(graph_filename) # verifies that the file can be loaded correctly + else: + graph_filename = f"{save_directory}/block-{self.block.item()}.pt" + state_dict = self.state_dict() + with open(graph_filename, "wb") as graph_file: + pickle.dump(state_dict, graph_file) + return self + + def load(self, root_dir: Optional[list[str]] = None) -> None: + """ + Loads the state of the metagraph from the default save directory. This method is instrumental for restoring the + metagraph to its last saved state. It automatically identifies the save directory based on the ``network`` and + ``netuid`` properties of the metagraph, locates the latest block file in that directory, and loads all metagraph + parameters from it. + + This functionality is particularly beneficial when continuity in the state of the metagraph is necessary + across different runtime sessions, or after a restart of the system. It ensures that the metagraph reflects + the exact state it was in at the last save point, maintaining consistency in the network's representation. + + The method delegates to ``load_from_path``, supplying it with the directory path constructed from the + metagraph's current ``network`` and ``netuid`` properties. This abstraction simplifies the process of loading + the metagraph's state for the user, requiring no direct path specifications. + + Args: + root_dir: list to the file path for the root directory of your metagraph saves + (i.e. ['/', 'tmp', 'metagraphs'], defaults to ["~", ".bittensor", "metagraphs"] + + Returns: + metagraph (bittensor.core.metagraph.Metagraph): The metagraph instance after loading its state from the + default directory. + + Example: + Load the metagraph state from the last saved snapshot in the default directory:: + + metagraph.load() + + After this operation, the metagraph's parameters and neuron data are restored to their state at the time of + the last save in the default directory. + + Note: + The default save directory is determined based on the metagraph's ``network`` and ``netuid`` attributes. It + is important to ensure that these attributes are set correctly and that the default save directory contains + the appropriate state files for the metagraph. + """ + self.load_from_path(get_save_dir(self.network, self.netuid, root_dir=root_dir)) + + @abstractmethod + def load_from_path(self, dir_path: str) -> "AsyncMetagraph": + """ + Loads the state of the metagraph from a specified directory path. This method is crucial for restoring the + metagraph to a specific state based on saved data. It locates the latest block file in the given directory and + loads all metagraph parameters from it. This is particularly useful for analyses that require historical states + of the network or for restoring previous states of the metagraph in different execution environments. + + The method first identifies the latest block file in the specified directory, then loads the metagraph state + including neuron attributes and parameters from this file. This ensures that the metagraph is accurately + reconstituted to reflect the network state at the time of the saved block. + + Args: + dir_path (str): The directory path where the metagraph's state files are stored. This path should contain + one or more saved state files, typically named in a format that includes the block number. + + Returns: + metagraph (bittensor.core.metagraph.AsyncMetagraph): The metagraph instance after loading its state from the + specified directory path. + + Example: + Load the metagraph state from a specific directory:: + + dir_path = "/path/to/saved/metagraph/states" + metagraph.load_from_path(dir_path) + + The metagraph is now restored to the state it was in at the time of the latest saved block in the specified + directory. + + Note: + This method assumes that the state files in the specified directory are correctly formatted and + contain valid data for the metagraph. It is essential to ensure that the directory path and the + state files within it are accurate and consistent with the expected metagraph structure. + """ + + def __deepcopy__(self, memo): + cls = self.__class__ + new_instance = cls.__new__(cls) + memo[id(self)] = new_instance + + for key, value in self.__dict__.items(): + if key == "subtensor": + setattr(new_instance, key, None) + else: + setattr(new_instance, key, copy.deepcopy(value, memo)) + + return new_instance + + def __copy__(self): + cls = self.__class__ + new_instance = cls.__new__(cls) + + for key, value in self.__dict__.items(): + if key == "subtensor": + setattr(new_instance, key, None) + else: + setattr(new_instance, key, value) + return new_instance + + def _apply_metagraph_info_mixin(self, metagraph_info: "MetagraphInfo"): + """ + Updates the attributes of the current object with data from a provided MetagraphInfo instance. + + Args: + metagraph_info (MetagraphInfo): An instance of the MetagraphInfo class containing the data to be applied to + the current object. + """ + self.name = metagraph_info.name + self.symbol = metagraph_info.symbol + self.network_registered_at = metagraph_info.network_registered_at + self.num_uids = metagraph_info.num_uids + self.max_uids = metagraph_info.max_uids + self.identities = metagraph_info.identities + self.identity = metagraph_info.identity + self.pruning_score = metagraph_info.pruning_score + self.block_at_registration = metagraph_info.block_at_registration + self.tao_dividends_per_hotkey = [ + (h, d.tao) for (h, d) in metagraph_info.tao_dividends_per_hotkey + ] + self.alpha_dividends_per_hotkey = [ + (a, d.tao) for (a, d) in metagraph_info.alpha_dividends_per_hotkey + ] + self.last_step = metagraph_info.last_step + self.tempo = metagraph_info.tempo + self.blocks_since_last_step = metagraph_info.blocks_since_last_step + self.owner_coldkey = metagraph_info.owner_coldkey + self.owner_hotkey = metagraph_info.owner_hotkey + + self.hparams = MetagraphInfoParams( + activity_cutoff=metagraph_info.activity_cutoff, + adjustment_alpha=metagraph_info.adjustment_alpha, + adjustment_interval=metagraph_info.adjustment_interval, + alpha_high=metagraph_info.alpha_high, + alpha_low=metagraph_info.alpha_low, + bonds_moving_avg=metagraph_info.bonds_moving_avg, + burn=metagraph_info.burn.tao, + commit_reveal_period=metagraph_info.commit_reveal_period, + commit_reveal_weights_enabled=metagraph_info.commit_reveal_weights_enabled, + difficulty=metagraph_info.difficulty, + immunity_period=metagraph_info.immunity_period, + kappa=metagraph_info.kappa, + liquid_alpha_enabled=metagraph_info.liquid_alpha_enabled, + max_burn=metagraph_info.max_burn.tao, + max_difficulty=metagraph_info.max_difficulty, + max_regs_per_block=metagraph_info.max_regs_per_block, + max_validators=metagraph_info.max_validators, + max_weights_limit=metagraph_info.max_weights_limit, + min_allowed_weights=metagraph_info.min_allowed_weights, + min_burn=metagraph_info.min_burn.tao, + min_difficulty=metagraph_info.min_difficulty, + pow_registration_allowed=metagraph_info.pow_registration_allowed, + registration_allowed=metagraph_info.registration_allowed, + rho=metagraph_info.rho, + serving_rate_limit=metagraph_info.serving_rate_limit, + target_regs_per_interval=metagraph_info.target_regs_per_interval, + tempo=metagraph_info.tempo, + weights_rate_limit=metagraph_info.weights_rate_limit, + weights_version=metagraph_info.weights_version, + ) + self.pool = MetagraphInfoPool( + alpha_out=metagraph_info.alpha_out.tao, + alpha_in=metagraph_info.alpha_in.tao, + tao_in=metagraph_info.tao_in.tao, + subnet_volume=metagraph_info.subnet_volume.tao, + moving_price=metagraph_info.moving_price.tao, + ) + self.emissions = MetagraphInfoEmissions( + alpha_out_emission=metagraph_info.alpha_out_emission.tao, + alpha_in_emission=metagraph_info.alpha_in_emission.tao, + subnet_emission=metagraph_info.subnet_emission.tao, + tao_in_emission=metagraph_info.tao_in_emission.tao, + pending_alpha_emission=metagraph_info.pending_alpha_emission.tao, + pending_root_emission=metagraph_info.pending_root_emission.tao, + ) + + +if use_torch(): + BaseClass = torch.nn.Module +else: + BaseClass = object +""" +Base class that extends :class:`torch.nn.Module` if PyTorch is used; otherwise, it defaults to object. +""" + + +class TorchMetagraph(MetagraphMixin, BaseClass): + def __init__( + self, + netuid: int, + network: str = settings.DEFAULT_NETWORK, + lite: bool = True, + sync: bool = True, + subtensor: Optional[Union["AsyncSubtensor", "Subtensor"]] = None, + ): + """ + Initializes a new instance of the metagraph object, setting up the basic structure and parameters based on the + provided arguments. This class requires Torch to be installed. This method is the entry point for creating a + metagraph object, which is a central component in representing the state of the Bittensor network. + + Args: + netuid (int): The unique identifier for the network, distinguishing this instance of the metagraph within + potentially multiple network configurations. + network (str): The name of the network, which can indicate specific configurations or versions of the + Bittensor network. + lite (bool): A flag indicating whether to use a lite version of the metagraph. The lite version may contain + less detailed information but can be quicker to initialize and sync. + sync (bool): A flag indicating whether to synchronize the metagraph with the network upon initialization. + Synchronization involves updating the metagraph's parameters to reflect the current state of the network. + + Example: + Initializing a metagraph object for the Bittensor network with a specific network UID:: + + from bittensor.core.metagraph import Metagraph + + metagraph = Metagraph(netuid=123, network="finney", lite=True, sync=True) + """ + BaseClass.__init__(self) + MetagraphMixin.__init__(self, netuid, network, lite, sync, subtensor) + self._dtype_registry = { + "int64": torch.int64, + "float32": torch.float32, + "bool": torch.bool, + } + self.version = torch.nn.Parameter( + torch.tensor([settings.version_as_int], dtype=torch.int64), + requires_grad=False, + ) + self.n: torch.nn.Parameter = torch.nn.Parameter( + torch.tensor([0], dtype=torch.int64), requires_grad=False + ) + self.block: torch.nn.Parameter = torch.nn.Parameter( + torch.tensor([0], dtype=torch.int64), requires_grad=False + ) + self.stake = torch.nn.Parameter( + torch.tensor([], dtype=torch.float32), requires_grad=False + ) + self.total_stake: torch.nn.Parameter = torch.nn.Parameter( + torch.tensor([], dtype=torch.float32), requires_grad=False + ) + self.ranks: torch.nn.Parameter = torch.nn.Parameter( + torch.tensor([], dtype=torch.float32), requires_grad=False + ) + self.trust: torch.nn.Parameter = torch.nn.Parameter( + torch.tensor([], dtype=torch.float32), requires_grad=False + ) + self.consensus: torch.nn.Parameter = torch.nn.Parameter( + torch.tensor([], dtype=torch.float32), requires_grad=False + ) + self.validator_trust: torch.nn.Parameter = torch.nn.Parameter( + torch.tensor([], dtype=torch.float32), requires_grad=False + ) + self.incentive: torch.nn.Parameter = torch.nn.Parameter( + torch.tensor([], dtype=torch.float32), requires_grad=False + ) + self.emission: torch.nn.Parameter = torch.nn.Parameter( + torch.tensor([], dtype=torch.float32), requires_grad=False + ) + self.dividends: torch.nn.Parameter = torch.nn.Parameter( + torch.tensor([], dtype=torch.float32), requires_grad=False + ) + self.active = torch.nn.Parameter( + torch.tensor([], dtype=torch.int64), requires_grad=False + ) + self.last_update = torch.nn.Parameter( + torch.tensor([], dtype=torch.int64), requires_grad=False + ) + self.validator_permit = torch.nn.Parameter( + torch.tensor([], dtype=torch.bool), requires_grad=False + ) + self.weights: torch.nn.Parameter = torch.nn.Parameter( + torch.tensor([], dtype=torch.float32), requires_grad=False + ) + self.bonds: torch.nn.Parameter = torch.nn.Parameter( + torch.tensor([], dtype=torch.int64), requires_grad=False + ) + self.uids = torch.nn.Parameter( + torch.tensor([], dtype=torch.int64), requires_grad=False + ) + self.alpha_stake = torch.nn.Parameter( + torch.tensor([], dtype=torch.float32), requires_grad=False + ) + self.tao_stake = torch.nn.Parameter( + torch.tensor([], dtype=torch.float32), requires_grad=False + ) + + def load_from_path(self, dir_path: str) -> "MetagraphMixin": + """ + Loads the metagraph state from a specified directory path. + + Args: + dir_path (str): The directory path where the state file is located. + + Returns: + metagraph (bittensor.core.metagraph.AsyncMetagraph): The current metagraph instance with the loaded state. + + Example: + + from bittensor.core.metagraph import Metagraph + + netuid = 1 + metagraph = Metagraph(netuid=netuid) + + metagraph.load_from_path("/path/to/dir") + + """ + + graph_file = latest_block_path(dir_path) + with safe_globals(): + state_dict = torch.load(graph_file) + self.n = torch.nn.Parameter(state_dict["n"], requires_grad=False) + self.block = torch.nn.Parameter(state_dict["block"], requires_grad=False) + self.uids = torch.nn.Parameter(state_dict["uids"], requires_grad=False) + self.stake = torch.nn.Parameter(state_dict["stake"], requires_grad=False) + self.total_stake = torch.nn.Parameter( + state_dict["total_stake"], requires_grad=False + ) + self.ranks = torch.nn.Parameter(state_dict["ranks"], requires_grad=False) + self.trust = torch.nn.Parameter(state_dict["trust"], requires_grad=False) + self.consensus = torch.nn.Parameter( + state_dict["consensus"], requires_grad=False + ) + self.validator_trust = torch.nn.Parameter( + state_dict["validator_trust"], requires_grad=False + ) + self.incentive = torch.nn.Parameter( + state_dict["incentive"], requires_grad=False + ) + self.emission = torch.nn.Parameter(state_dict["emission"], requires_grad=False) + self.dividends = torch.nn.Parameter( + state_dict["dividends"], requires_grad=False + ) + self.active = torch.nn.Parameter(state_dict["active"], requires_grad=False) + self.last_update = torch.nn.Parameter( + state_dict["last_update"], requires_grad=False + ) + self.validator_permit = torch.nn.Parameter( + state_dict["validator_permit"], requires_grad=False + ) + self.uids = torch.nn.Parameter(state_dict["uids"], requires_grad=False) + self.axons = state_dict["axons"] + self.neurons = state_dict["neurons"] + if "weights" in state_dict: + self.weights = torch.nn.Parameter( + state_dict["weights"], requires_grad=False + ) + if "bonds" in state_dict: + self.bonds = torch.nn.Parameter(state_dict["bonds"], requires_grad=False) + return self + + +class NonTorchMetagraph(MetagraphMixin): + def __init__( + self, + netuid: int, + network: str = settings.DEFAULT_NETWORK, + lite: bool = True, + sync: bool = True, + subtensor: Optional[Union["AsyncSubtensor", "Subtensor"]] = None, + ): + """ + Initializes a new instance of the metagraph object, setting up the basic structure and parameters based on the + provided arguments. This class doesn't require installed Torch. This method is the entry point for creating a + metagraph object, which is a central component in representing the state of the Bittensor network. + + Args: + netuid (int): The unique identifier for the network, distinguishing this instance of the metagraph within + potentially multiple network configurations. + network (str): The name of the network, which can indicate specific configurations or versions of the + Bittensor network. + lite (bool): A flag indicating whether to use a lite version of the metagraph. The lite version may contain + less detailed information but can be quicker to initialize and sync. + sync (bool): A flag indicating whether to synchronize the metagraph with the network upon initialization. + Synchronization involves updating the metagraph's parameters to reflect the current state of the network. + + Example: + Initializing a metagraph object for the Bittensor network with a specific network UID:: + + from bittensor.core.metagraph import Metagraph + + metagraph = Metagraph(netuid=123, network="finney", lite=True, sync=True) + """ + MetagraphMixin.__init__(self, netuid, network, lite, sync, subtensor) + + self.netuid = netuid + self.network, self.chain_endpoint = determine_chain_endpoint_and_network( + network + ) + self.version = np.array([settings.version_as_int], dtype=np.int64) + self.n = np.array([0], dtype=np.int64) + self.block = np.array([0], dtype=np.int64) + self.ranks = np.array([], dtype=np.float32) + self.trust = np.array([], dtype=np.float32) + self.consensus = np.array([], dtype=np.float32) + self.validator_trust = np.array([], dtype=np.float32) + self.incentive = np.array([], dtype=np.float32) + self.emission = np.array([], dtype=np.float32) + self.dividends = np.array([], dtype=np.float32) + self.active = np.array([], dtype=np.int64) + self.last_update = np.array([], dtype=np.int64) + self.validator_permit = np.array([], dtype=bool) + self.weights = np.array([], dtype=np.float32) + self.bonds = np.array([], dtype=np.int64) + self.uids = np.array([], dtype=np.int64) + self.alpha_stake: Tensor = np.array([], dtype=np.int64) + self.tao_stake: Tensor = np.array([], dtype=np.int64) + self.stake: Tensor = np.array([], dtype=np.int64) + self.total_stake: Tensor = np.array([], dtype=np.int64) + self.subtensor = subtensor + self.should_sync = sync + + def load_from_path(self, dir_path: str) -> "MetagraphMixin": + """ + Loads the state of the Metagraph from a specified directory path. + + Args: + dir_path (str): The directory path where the metagraph's state file is located. + + Returns: + metagraph (:func:`bittensor.core.metagraph.AsyncMetagraph`): An instance of the Metagraph with the state + loaded from the file. + + Raises: + pickle.UnpicklingError: If there is an error unpickling the state file. + RuntimeError: If there is an error loading the state file using PyTorch. + ImportError: If there is an error importing PyTorch. + """ + graph_filename = latest_block_path(dir_path) + try: + with open(graph_filename, "rb") as graph_file: + state_dict = pickle.load(graph_file) + except pickle.UnpicklingError: + logging.info( + "Unable to load file. Attempting to restore metagraph using torch." + ) + logging.warning( + ":warning: This functionality exists to load metagraph state from legacy saves, but will not be supported in the future." + ) + try: + import torch as real_torch + + with safe_globals(): + state_dict = real_torch.load(graph_filename) + for key in METAGRAPH_STATE_DICT_NDARRAY_KEYS: + state_dict[key] = state_dict[key].detach().numpy() + del real_torch + except (RuntimeError, ImportError): + logging.error("Unable to load file. It may be corrupted.") + raise + + self.n = state_dict["n"] + self.block = state_dict["block"] + self.uids = state_dict["uids"] + self.stake = state_dict["stake"] + self.ranks = state_dict["ranks"] + self.trust = state_dict["trust"] + self.consensus = state_dict["consensus"] + self.validator_trust = state_dict["validator_trust"] + self.incentive = state_dict["incentive"] + self.emission = state_dict["emission"] + self.dividends = state_dict["dividends"] + self.active = state_dict["active"] + self.last_update = state_dict["last_update"] + self.validator_permit = state_dict["validator_permit"] + self.axons = state_dict["axons"] + self.neurons = state_dict["neurons"] + if "weights" in state_dict: + self.weights = state_dict["weights"] + if "bonds" in state_dict: + self.bonds = state_dict["bonds"] + return self + + +if use_torch(): + NumpyOrTorch = TorchMetagraph +else: + NumpyOrTorch = NonTorchMetagraph +"""Metagraph class that uses :class:`TorchMetaGraph` if PyTorch is available; otherwise, it falls back to :class:`NonTorchMetagraph`. + +- **With PyTorch**: When `use_torch()` returns `True`, `Metagraph` is set to :class:`TorchMetaGraph`, which utilizes PyTorch functionalities. +- **Without PyTorch**: When `use_torch()` returns `False`, `Metagraph` is set to :class:`NonTorchMetagraph`, which does not rely on PyTorch. +""" + + +class AsyncMetagraph(NumpyOrTorch): + """ + TODO docstring. Advise user to use `async_metagraph` factory fn if they want to sync at init + """ + + def __init__( + self, + netuid: int, + network: str = settings.DEFAULT_NETWORK, + lite: bool = True, + sync: bool = True, + subtensor: Optional["AsyncSubtensor"] = None, + ): + super().__init__(netuid, network, lite, sync, subtensor) + + async def __aenter__(self): + if self.should_sync: + await self.sync(block=None, lite=self.lite, subtensor=self.subtensor) + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + pass + + async def sync( + self, + block: Optional[int] = None, + lite: Optional[bool] = None, + subtensor: Optional["AsyncSubtensor"] = None, + ): + """ + Synchronizes the metagraph with the Bittensor network's current state. It updates the metagraph's attributes to + reflect the latest data from the network, ensuring the metagraph represents the most current state of the + network. + + Args: + block (Optional[int]): A specific block number to synchronize with. If None, the metagraph syncs with the + latest block. This allows for historical analysis or specific state examination of the network. + lite (Optional[bool]): If True, a lite version of the metagraph is used for quicker synchronization. This is + beneficial when full detail is not necessary, allowing for reduced computational and time overhead. + Defaults to `True`. + subtensor (Optional[bittensor.core.subtensor.Subtensor]): An instance of the subtensor class from Bittensor, + providing an interface to the underlying blockchain data. If provided, this instance is used for data + retrieval during synchronization. + + Example: + Sync the metagraph with the latest block from the subtensor, using the lite version for efficiency:: + + from bittensor.core.subtensor import Subtensor + + subtensor = Subtensor() + metagraph.sync(subtensor=subtensor) + + Sync with a specific block number for detailed analysis:: + + from bittensor.core.subtensor import Subtensor + + subtensor = Subtensor() + metagraph.sync(block=12345, lite=False, subtensor=subtensor) + + NOTE: + If attempting to access data beyond the previous 300 blocks, you **must** use the ``archive`` network for + subtensor. Light nodes are configured only to store the previous 300 blocks if connecting to finney or + test networks. + + For example:: + + from bittensor.core.subtensor import Subtensor + + subtensor = Subtensor(network='archive') + current_block = subtensor.get_current_block() + history_block = current_block - 1200 + + metagraph.sync(block=history_block, lite=False, subtensor=subtensor) + """ + if lite is None: + lite = self.lite + + subtensor = await self._initialize_subtensor(subtensor) + + if ( + subtensor.chain_endpoint != settings.ARCHIVE_ENTRYPOINT + or subtensor.network != "archive" + ): + cur_block = await subtensor.get_current_block() + if block and block < (cur_block - 300): + logging.warning( + "Attempting to sync longer than 300 blocks ago on a non-archive node. Please use the 'archive' " + "network for subtensor and retry." + ) + if block is None: + block = await subtensor.get_current_block() + + # Assign neurons based on 'lite' flag + await self._assign_neurons(block, lite, subtensor) + + # Set attributes for metagraph + self._set_metagraph_attributes(block) + + # If not a 'lite' version, compute and set weights and bonds for each neuron + if not lite: + await self._set_weights_and_bonds(subtensor=subtensor, block=block) + + # Fills in the stake associated attributes of a class instance from a chain response. + await self._get_all_stakes_from_chain(block=block) + + # apply MetagraphInfo data to instance + await self._apply_metagraph_info(block=block) + + async def _initialize_subtensor( + self, subtensor: "AsyncSubtensor" + ) -> "AsyncSubtensor": + """ + Initializes the subtensor to be used for syncing the metagraph. + + This method ensures that a subtensor instance is available and properly set up for data retrieval during the + synchronization process. + + If no subtensor is provided, this method is responsible for creating a new instance of the subtensor, configured + according to the current network settings. + + Args: + subtensor (bittensor.core.async_subtensor.AsyncSubtensor): The subtensor instance provided for + initialization. If ``None``, a new subtensor instance is created using the current network configuration. + + Returns: + subtensor (bittensor.core.async_subtensor.AsyncSubtensor): The initialized subtensor instance, ready to be + used for syncing the metagraph. + + Internal Usage: + Used internally during the sync process to ensure a valid subtensor instance is available:: + + subtensor = await self._initialize_subtensor(subtensor) + """ + if subtensor and subtensor != self.subtensor: + self.subtensor = subtensor + if not subtensor and self.subtensor: + subtensor = self.subtensor + if not subtensor: + # Lazy import due to circular import (subtensor -> metagraph, metagraph -> subtensor) + from bittensor.core.async_subtensor import AsyncSubtensor + + async with AsyncSubtensor(network=self.chain_endpoint) as subtensor: + self.subtensor = subtensor + return subtensor + + async def _assign_neurons( + self, block: int, lite: bool, subtensor: "AsyncSubtensor" + ): + """ + Assigns neurons to the metagraph based on the provided block number and the lite flag. + + This method is responsible for fetching and setting the neuron data in the metagraph, which includes neuron + attributes like UID, stake, trust, and other relevant information. + + Args: + block (int): The block number for which the neuron data needs to be fetched. If ``None``, the latest block + data is used. + lite (bool): A boolean flag indicating whether to use a lite version of the neuron data. The lite version + typically includes essential information and is quicker to fetch and process. + subtensor (bittensor.core.async_subtensor.AsyncSubtensor): The subtensor instance used for fetching neuron + data from the network. + + Internal Usage: + Used internally during the sync process to fetch and set neuron data:: + + from bittensor.core.subtensor import Subtensor + + block = 12345 + lite = False + subtensor = Subtensor() + self._assign_neurons(block, lite, subtensor) + """ + if lite: + self.neurons = await subtensor.neurons_lite(block=block, netuid=self.netuid) + + else: + self.neurons = await subtensor.neurons(block=block, netuid=self.netuid) + self.lite = lite + + async def _set_weights_and_bonds(self, subtensor: "AsyncSubtensor", block: int): + """ + Computes and sets the weights and bonds for each neuron in the metagraph. This method is responsible for + processing the raw weight and bond data obtained from the network and converting it into a structured format + suitable for the metagraph model. + + Args: + subtensor: The subtensor instance used for fetching weights and bonds data. If ``None``, the weights and + bonds are not updated. + + Internal Usage: + Used internally during the sync process to update the weights and bonds of the neurons:: + + self._set_weights_and_bonds(subtensor=subtensor) + """ + # TODO: Check and test the computation of weights and bonds + if self.netuid == 0: + self.weights = await self._process_root_weights( + [neuron.weights for neuron in self.neurons], + "weights", + subtensor, + block=block, + ) + else: + self.weights = self._process_weights_or_bonds( + [neuron.weights for neuron in self.neurons], "weights" + ) + self.bonds = self._process_weights_or_bonds( + [neuron.bonds for neuron in self.neurons], "bonds" + ) + + async def _process_root_weights( + self, data: list, attribute: str, subtensor: "AsyncSubtensor", block: int + ) -> Union[NDArray, "torch.nn.Parameter"]: + """ + Specifically processes the root weights data for the metagraph. This method is similar to :func:`_process_weights_or_bonds` + but is tailored for processing root weights, which have a different structure and significance in the network. + + Args: + data (list): The raw root weights data to be processed. + attribute (str): A string indicating the attribute type, here it's typically ``weights``. + subtensor (bittensor.core.async_subtensor.AsyncSubtensor): The subtensor instance used for additional data + and context needed in processing. + + Returns: + A tensor parameter encapsulating the processed root weights data. + + Internal Usage: + Used internally to process and set root weights for the metagraph:: + + self.root_weights = self._process_root_weights(raw_root_weights_data, "weights", subtensor) + """ + data_array = [] + n_subnets_, subnets = await asyncio.gather( + subtensor.get_total_subnets(block=block), subtensor.get_subnets(block=block) + ) + n_subnets = n_subnets_ or 0 + for item in data: + if len(item) == 0: + if use_torch(): + data_array.append(torch.zeros(n_subnets)) + else: + data_array.append(np.zeros(n_subnets, dtype=np.float32)) + else: + uids, values = zip(*item) + # TODO: Validate and test the conversion of uids and values to tensor + data_array.append( + convert_root_weight_uids_and_vals_to_tensor( + n_subnets, list(uids), list(values), subnets + ) + ) + + tensor_param: Union[NDArray, "torch.nn.Parameter"] = ( + ( + torch.nn.Parameter(torch.stack(data_array), requires_grad=False) + if len(data_array) + else torch.nn.Parameter() + ) + if use_torch() + else ( + np.stack(data_array) + if len(data_array) + else np.array([], dtype=np.float32) + ) + ) + if len(data_array) == 0: + logging.warning( + f"Empty {attribute}_array on metagraph.sync(). The '{attribute}' tensor is empty." + ) + return tensor_param + + async def _get_all_stakes_from_chain(self, block: int): + """Fills in the stake associated attributes of a class instance from a chain response.""" + try: + result = await self.subtensor.query_runtime_api( + runtime_api="SubnetInfoRuntimeApi", + method="get_subnet_state", + params=[self.netuid], + block=block, + ) + + if result is None: + logging.debug( + f"Unable to retrieve subnet state for netuid `{self.netuid}`." + ) + return [] + + subnet_state: "SubnetState" = SubnetState.from_dict(result) + + if self.netuid == 0: + self.total_stake = self.stake = self.tao_stake = self.alpha_stake = ( + self._create_tensor( + [stake.tao for stake in subnet_state.tao_stake], + dtype=self._dtype_registry["float32"], + ) + ) + return subnet_state + + self.alpha_stake = self._create_tensor( + [b.tao for b in subnet_state.alpha_stake], + dtype=self._dtype_registry["float32"], + ) + self.tao_stake = self._create_tensor( + [ + b.tao * settings.ROOT_TAO_STAKE_WEIGHT + for b in subnet_state.tao_stake + ], + dtype=self._dtype_registry["float32"], + ) + self.total_stake = self.stake = self._create_tensor( + [stake.tao for stake in subnet_state.total_stake], + dtype=self._dtype_registry["float32"], + ) + return subnet_state + except (SubstrateRequestException, AttributeError) as e: + logging.debug(e) + + async def _apply_metagraph_info(self, block: int): + """Retrieves metagraph information for a specific subnet and applies it using a mixin.""" + metagraph_info = await self.subtensor.get_metagraph_info( + self.netuid, block=block + ) + if metagraph_info: + self._apply_metagraph_info_mixin(metagraph_info=metagraph_info) + + +class Metagraph(NumpyOrTorch): + def __init__( + self, + netuid: int, + network: str = settings.DEFAULT_NETWORK, + lite: bool = True, + sync: bool = True, + subtensor: Optional["Subtensor"] = None, + ): + super().__init__(netuid, network, lite, sync, subtensor) + if self.should_sync: + self.sync() + + def sync( + self, + block: Optional[int] = None, + lite: Optional[bool] = None, + subtensor: Optional["Subtensor"] = None, + ): + """ + Synchronizes the metagraph with the Bittensor network's current state. It updates the metagraph's attributes to + reflect the latest data from the network, ensuring the metagraph represents the most current state of the + network. + + Args: + block (Optional[int]): A specific block number to synchronize with. If None, the metagraph syncs with the + latest block. This allows for historical analysis or specific state examination of the network. + lite (Optional[bool]): If True, a lite version of the metagraph is used for quicker synchronization. This is + beneficial when full detail is not necessary, allowing for reduced computational and time overhead. + Defaults to `True`. + subtensor (Optional[bittensor.core.subtensor.Subtensor]): An instance of the subtensor class from Bittensor, + providing an interface to the underlying blockchain data. If provided, this instance is used for data + retrieval during synchronization. + + Example: + Sync the metagraph with the latest block from the subtensor, using the lite version for efficiency:: + + from bittensor.core.subtensor import Subtensor + + subtensor = Subtensor() + metagraph.sync(subtensor=subtensor) + + Sync with a specific block number for detailed analysis:: + + from bittensor.core.subtensor import Subtensor + + subtensor = Subtensor() + metagraph.sync(block=12345, lite=False, subtensor=subtensor) + + NOTE: + If attempting to access data beyond the previous 300 blocks, you **must** use the ``archive`` network for + subtensor. Light nodes are configured only to store the previous 300 blocks if connecting to finney or + test networks. + + For example:: + + from bittensor.core.subtensor import Subtensor + + subtensor = Subtensor(network='archive') + current_block = subtensor.get_current_block() + history_block = current_block - 1200 + + metagraph.sync(block=history_block, lite=False, subtensor=subtensor) + """ + if lite is None: + lite = self.lite + + # Initialize subtensor + subtensor = self._initialize_subtensor(subtensor=subtensor) + + if ( + subtensor.chain_endpoint != settings.ARCHIVE_ENTRYPOINT + or subtensor.network != "archive" + ): + cur_block = subtensor.get_current_block() + if block and block < (cur_block - 300): + logging.warning( + "Attempting to sync longer than 300 blocks ago on a non-archive node. Please use the 'archive' " + "network for subtensor and retry." + ) + + if block is None: + block = subtensor.get_current_block() + + # Assign neurons based on 'lite' flag + self._assign_neurons(block, lite, subtensor) + + # Set attributes for metagraph + self._set_metagraph_attributes(block) + + # If not a 'lite' version, compute and set weights and bonds for each neuron + if not lite: + self._set_weights_and_bonds(subtensor=subtensor, block=block) + + # Fills in the stake associated attributes of a class instance from a chain response. + self._get_all_stakes_from_chain(block=block) + + # apply MetagraphInfo data to instance + self._apply_metagraph_info(block=block) + + def _initialize_subtensor(self, subtensor: "Subtensor") -> "Subtensor": + """ + Initializes the subtensor to be used for syncing the metagraph. + + This method ensures that a subtensor instance is available and properly set up for data retrieval during the + synchronization process. + + If no subtensor is provided, this method is responsible for creating a new instance of the subtensor, configured + according to the current network settings. + + Args: + subtensor (bittensor.core.subtensor.Subtensor): The subtensor instance provided for + initialization. If ``None``, a new subtensor instance is created using the current network configuration. + + Returns: + subtensor (bittensor.core.subtensor.Subtensor): The initialized subtensor instance, ready to be + used for syncing the metagraph. + + Internal Usage: + Used internally during the sync process to ensure a valid subtensor instance is available:: + + subtensor = self._initialize_subtensor(subtensor) + """ + if subtensor and subtensor != self.subtensor: + self.subtensor = subtensor + if not subtensor and self.subtensor: + subtensor = self.subtensor + if not subtensor: + # Lazy import due to circular import (subtensor -> metagraph, metagraph -> subtensor) + from bittensor.core.subtensor import Subtensor + + subtensor = Subtensor(network=self.chain_endpoint) + + self.subtensor = subtensor + return subtensor + + def _assign_neurons(self, block: int, lite: bool, subtensor: "Subtensor"): + """ + Assigns neurons to the metagraph based on the provided block number and the lite flag. + + This method is responsible for fetching and setting the neuron data in the metagraph, which includes neuron + attributes like UID, stake, trust, and other relevant information. + + Args: + block (int): The block number for which the neuron data needs to be fetched. If ``None``, the latest block + data is used. + lite (bool): A boolean flag indicating whether to use a lite version of the neuron data. The lite version + typically includes essential information and is quicker to fetch and process. + subtensor (bittensor.core.async_subtensor.AsyncSubtensor): The subtensor instance used for fetching neuron + data from the network. + + Internal Usage: + Used internally during the sync process to fetch and set neuron data:: + + from bittensor.core.subtensor import Subtensor + + block = 12345 + lite = False + subtensor = Subtensor() + self._assign_neurons(block, lite, subtensor) + """ + if lite: + self.neurons = subtensor.neurons_lite(block=block, netuid=self.netuid) + + else: + self.neurons = subtensor.neurons(block=block, netuid=self.netuid) + self.lite = lite + + def _set_weights_and_bonds(self, block: int, subtensor: "Subtensor"): + """ + Computes and sets the weights and bonds for each neuron in the metagraph. This method is responsible for + processing the raw weight and bond data obtained from the network and converting it into a structured format + suitable for the metagraph model. + + Args: + subtensor: The subtensor instance used for fetching weights and bonds data. If ``None``, the weights and + bonds are not updated. + + Internal Usage: + Used internally during the sync process to update the weights and bonds of the neurons:: + + self._set_weights_and_bonds(subtensor=subtensor) + """ + if self.netuid == 0: + self.weights = self._process_root_weights( + [neuron.weights for neuron in self.neurons], "weights", subtensor, block + ) + else: + self.weights = self._process_weights_or_bonds( + [neuron.weights for neuron in self.neurons], "weights" + ) + self.bonds = self._process_weights_or_bonds( + [neuron.bonds for neuron in self.neurons], "bonds" + ) + + def _process_root_weights( + self, data: list, attribute: str, subtensor: "Subtensor", block: int + ) -> Union[NDArray, "torch.nn.Parameter"]: + """ + Specifically processes the root weights data for the metagraph. This method is similar to :func:`_process_weights_or_bonds` + but is tailored for processing root weights, which have a different structure and significance in the network. + + Args: + data (list): The raw root weights data to be processed. + attribute (str): A string indicating the attribute type, here it's typically ``weights``. + subtensor (bittensor.core.async_subtensor.AsyncSubtensor): The subtensor instance used for additional data + and context needed in processing. + + Returns: + A tensor parameter encapsulating the processed root weights data. + + Internal Usage: + Used internally to process and set root weights for the metagraph:: + + self.root_weights = self._process_root_weights(raw_root_weights_data, "weights", subtensor) + """ + data_array = [] + n_subnets = subtensor.get_total_subnets(block=block) or 0 + subnets = subtensor.get_subnets(block=block) + for item in data: + if len(item) == 0: + if use_torch(): + data_array.append(torch.zeros(n_subnets)) + else: + data_array.append(np.zeros(n_subnets, dtype=np.float32)) + else: + uids, values = zip(*item) + # TODO: Validate and test the conversion of uids and values to tensor + data_array.append( + convert_root_weight_uids_and_vals_to_tensor( + n_subnets, list(uids), list(values), subnets + ) + ) + + tensor_param: Union[NDArray, "torch.nn.Parameter"] = ( + ( + torch.nn.Parameter(torch.stack(data_array), requires_grad=False) + if len(data_array) + else torch.nn.Parameter() + ) + if use_torch() + else ( + np.stack(data_array) + if len(data_array) + else np.array([], dtype=np.float32) + ) + ) + if len(data_array) == 0: + logging.warning( + f"Empty {attribute}_array on metagraph.sync(). The '{attribute}' tensor is empty." + ) + return tensor_param + + def _get_all_stakes_from_chain(self, block: int): + """Fills in the stake associated attributes of a class instance from a chain response.""" + try: + result = self.subtensor.query_runtime_api( + runtime_api="SubnetInfoRuntimeApi", + method="get_subnet_state", + params=[self.netuid], + block=block, + ) + + if result is None: + logging.debug( + f"Unable to retrieve subnet state for netuid `{self.netuid}`." + ) + return [] + + subnet_state: "SubnetState" = SubnetState.from_dict(result) + + if self.netuid == 0: + self.total_stake = self.stake = self.tao_stake = self.alpha_stake = ( + self._create_tensor( + [stake.tao for stake in subnet_state.tao_stake], + dtype=self._dtype_registry["float32"], + ) + ) + return subnet_state + + self.alpha_stake = self._create_tensor( + [b.tao for b in subnet_state.alpha_stake], + dtype=self._dtype_registry["float32"], + ) + self.tao_stake = self._create_tensor( + [ + b.tao * settings.ROOT_TAO_STAKE_WEIGHT + for b in subnet_state.tao_stake + ], + dtype=self._dtype_registry["float32"], + ) + self.total_stake = self.stake = self._create_tensor( + [stake.tao for stake in subnet_state.total_stake], + dtype=self._dtype_registry["float32"], + ) + return subnet_state + except (SubstrateRequestException, AttributeError) as e: + logging.debug(e) + + def _apply_metagraph_info(self, block: int): + """Retrieves metagraph information for a specific subnet and applies it using a mixin.""" + metagraph_info = self.subtensor.get_metagraph_info(self.netuid, block=block) + if metagraph_info: + self._apply_metagraph_info_mixin(metagraph_info=metagraph_info) + + +async def async_metagraph( + netuid: int, + network: str = settings.DEFAULT_NETWORK, + lite: bool = True, + sync: bool = True, + subtensor: "AsyncSubtensor" = None, +) -> "AsyncMetagraph": + """ + Factory function to create an instantiated AsyncMetagraph, mainly for the ability to use sync at instantiation. + """ + metagraph_ = AsyncMetagraph( + netuid=netuid, network=network, lite=lite, sync=sync, subtensor=subtensor + ) + if sync: + await metagraph_.sync() + return metagraph_ diff --git a/bittensor/core/settings.py b/bittensor/core/settings.py new file mode 100644 index 0000000000..3e5edae0d6 --- /dev/null +++ b/bittensor/core/settings.py @@ -0,0 +1,164 @@ +import os +import importlib.metadata +import re +from pathlib import Path + +from munch import munchify + +ROOT_TAO_STAKE_WEIGHT = 0.18 + +READ_ONLY = os.getenv("READ_ONLY") == "1" + +HOME_DIR = Path.home() +USER_BITTENSOR_DIR = HOME_DIR / ".bittensor" +WALLETS_DIR = USER_BITTENSOR_DIR / "wallets" +MINERS_DIR = USER_BITTENSOR_DIR / "miners" + +__version__ = importlib.metadata.version("bittensor") + + +if not READ_ONLY: + # Create dirs if they don't exist + WALLETS_DIR.mkdir(parents=True, exist_ok=True) + MINERS_DIR.mkdir(parents=True, exist_ok=True) + +# Bittensor networks name +NETWORKS = ["finney", "test", "archive", "local", "subvortex", "latent-lite"] + +# Bittensor endpoints (Needs to use wss://) +FINNEY_ENTRYPOINT = "wss://entrypoint-finney.opentensor.ai:443" +FINNEY_TEST_ENTRYPOINT = "wss://test.finney.opentensor.ai:443" +ARCHIVE_ENTRYPOINT = "wss://archive.chain.opentensor.ai:443" +LOCAL_ENTRYPOINT = os.getenv("BT_SUBTENSOR_CHAIN_ENDPOINT") or "ws://127.0.0.1:9944" +SUBVORTEX_ENTRYPOINT = "ws://subvortex.info:9944" +LATENT_LITE_ENTRYPOINT = "wss://lite.sub.latent.to:443" + +NETWORK_MAP = { + NETWORKS[0]: FINNEY_ENTRYPOINT, + NETWORKS[1]: FINNEY_TEST_ENTRYPOINT, + NETWORKS[2]: ARCHIVE_ENTRYPOINT, + NETWORKS[3]: LOCAL_ENTRYPOINT, + NETWORKS[4]: SUBVORTEX_ENTRYPOINT, + NETWORKS[5]: LATENT_LITE_ENTRYPOINT, +} + +REVERSE_NETWORK_MAP = { + FINNEY_ENTRYPOINT: NETWORKS[0], + FINNEY_TEST_ENTRYPOINT: NETWORKS[1], + ARCHIVE_ENTRYPOINT: NETWORKS[2], + LOCAL_ENTRYPOINT: NETWORKS[3], + SUBVORTEX_ENTRYPOINT: NETWORKS[4], + LATENT_LITE_ENTRYPOINT: NETWORKS[5], +} + +DEFAULT_NETWORK = NETWORKS[0] +DEFAULT_ENDPOINT = NETWORK_MAP[DEFAULT_NETWORK] + +# Currency Symbols Bittensor +TAO_SYMBOL: str = chr(0x03C4) +RAO_SYMBOL: str = chr(0x03C1) + +# Pip address for versioning +PIPADDRESS = "https://pypi.org/pypi/bittensor/json" + +# Substrate chain block time (seconds). +BLOCKTIME = 12 + +# Substrate ss58_format +SS58_FORMAT = 42 + +# Wallet ss58 address length +SS58_ADDRESS_LENGTH = 48 + +# Block Explorers map network to explorer url +# Must all be polkadotjs explorer urls +NETWORK_EXPLORER_MAP = { + "opentensor": { + "local": "https://polkadot.js.org/apps/?rpc=wss%3A%2F%2Fentrypoint-finney.opentensor.ai%3A443#/explorer", + "endpoint": "https://polkadot.js.org/apps/?rpc=wss%3A%2F%2Fentrypoint-finney.opentensor.ai%3A443#/explorer", + "finney": "https://polkadot.js.org/apps/?rpc=wss%3A%2F%2Fentrypoint-finney.opentensor.ai%3A443#/explorer", + }, + "taostats": { + "local": "https://taostats.io", + "endpoint": "https://taostats.io", + "finney": "https://taostats.io", + }, +} + +# --- Type Registry --- +TYPE_REGISTRY: dict[str, dict] = { + "types": { + "Balance": "u64", # Need to override default u128 + }, +} + + +_BT_AXON_PORT = os.getenv("BT_AXON_PORT") +_BT_AXON_MAX_WORKERS = os.getenv("BT_AXON_MAX_WORKERS") +_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": os.getenv("BT_LOGGING_DEBUG") or False, + "trace": os.getenv("BT_LOGGING_TRACE") or False, + "info": os.getenv("BT_LOGGING_INFO") or False, + "record_log": os.getenv("BT_LOGGING_RECORD_LOG") or False, + "logging_dir": os.getenv("BT_LOGGING_LOGGING_DIR") or str(MINERS_DIR), + }, + "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_CHAIN_ENDPOINT") or DEFAULT_ENDPOINT, + "network": os.getenv("BT_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), + }, + } +) + + +# Parsing version without any literals. +__version__ = re.match(r"^\d+\.\d+\.\d+", __version__).group(0) + +version_split = __version__.split(".") +_version_info = tuple(int(part) for part in version_split) +_version_int_base = 1000 +assert max(_version_info) < _version_int_base + +version_as_int: int = sum( + e * (_version_int_base**i) for i, e in enumerate(reversed(_version_info)) +) +assert version_as_int < 2**31 # fits in int32 + + +def __apply_nest_asyncio(): + """ + Apply nest_asyncio if the environment variable NEST_ASYNCIO is set to "1" or not set. + If not set, warn the user that the default will change in the future. + """ + nest_asyncio_env = os.getenv("NEST_ASYNCIO") + if nest_asyncio_env == "1": + # Install and apply nest asyncio to allow the async functions to run in a .ipynb + import nest_asyncio + + nest_asyncio.apply() + + +__apply_nest_asyncio() diff --git a/bittensor/core/stream.py b/bittensor/core/stream.py new file mode 100644 index 0000000000..628a459501 --- /dev/null +++ b/bittensor/core/stream.py @@ -0,0 +1,156 @@ +from abc import ABC, abstractmethod +from typing import Callable, Awaitable, Optional + +from aiohttp import ClientResponse +from pydantic import ConfigDict, BaseModel +from starlette.responses import StreamingResponse as _StreamingResponse +from starlette.types import Send, Receive, Scope + +from .synapse import Synapse + + +class BTStreamingResponseModel(BaseModel): + """ + :func:`BTStreamingResponseModel` is a Pydantic model that encapsulates the token streamer callable for Pydantic + validation. + It is used within the :func:`StreamingSynapse` class to create a :func:`BTStreamingResponse` object, which is + responsible for handling the streaming of tokens. + + The token streamer is a callable that takes a send function and returns an awaitable. It is responsible for generating + the content of the streaming response, typically by processing tokens and sending them to the client. + + This model ensures that the token streamer conforms to the expected signature and provides a clear interface for + passing the token streamer to the BTStreamingResponse class. + + Attributes: + token_streamer: Callable[[Send], Awaitable[None]] The token streamer callable, which takes a send function + (provided by the ASGI server) and returns an awaitable. It is responsible for generating the content of the + streaming response. + """ + + token_streamer: Callable[[Send], Awaitable[None]] + + +class StreamingSynapse(Synapse, ABC): + """ + The :func:`StreamingSynapse` class is designed to be subclassed for handling streaming responses in the Bittensor network. + It provides abstract methods that must be implemented by the subclass to deserialize, process streaming responses, + and extract JSON data. It also includes a method to create a streaming response object. + """ + + model_config = ConfigDict(validate_assignment=True) + + class BTStreamingResponse(_StreamingResponse): + """ + :func:`BTStreamingResponse` is a specialized subclass of the Starlette StreamingResponse designed to handle the + streaming of tokens within the Bittensor network. It is used internally by the StreamingSynapse class to manage + the response streaming process, including sending headers and calling the token streamer provided by the subclass. + + This class is not intended to be directly instantiated or modified by developers subclassing StreamingSynapse. + Instead, it is used by the :func:`create_streaming_response` method to create a response object based on the + token streamer provided by the subclass. + """ + + def __init__( + self, + model: "BTStreamingResponseModel", + *, + synapse: "Optional[StreamingSynapse]" = None, + **kwargs, + ): + """ + Initializes the BTStreamingResponse with the given token streamer model. + + Args: + model (bittensor.core.stream.BTStreamingResponseModel): A BTStreamingResponseModel instance containing + the token streamer callable, which is responsible for generating the content of the response. + synapse (bittensor.core.stream.StreamingSynapse): The response Synapse to be used to update the response + headers etc. + **kwargs: Additional keyword arguments passed to the parent StreamingResponse class. + """ + super().__init__(content=iter(()), **kwargs) + self.token_streamer = model.token_streamer + self.synapse = synapse + + async def stream_response(self, send: "Send"): + """ + Asynchronously streams the response by sending headers and calling the token streamer. + + This method is responsible for initiating the response by sending the appropriate headers, including the + content type for event-streaming. It then calls the token streamer to generate the content and sends the + response body to the client. + + Args: + send (starlette.types.Send): A callable to send the response, provided by the ASGI server. + """ + headers = [(b"content-type", b"text/event-stream")] + self.raw_headers + + await send( + {"type": "http.response.start", "status": 200, "headers": headers} + ) + + await self.token_streamer(send) + + await send({"type": "http.response.body", "body": b"", "more_body": False}) + + async def __call__(self, scope: "Scope", receive: "Receive", send: "Send"): + """ + Asynchronously calls the :func:`stream_response method`, allowing the :func:`BTStreamingResponse` object to + be used as an ASGI application. + + This method is part of the ASGI interface and is called by the ASGI server to handle the request and send + the response. It delegates to the :func:`stream_response` method to perform the actual streaming process. + + Args: + scope (starlette.types.Scope): The scope of the request, containing information about the client, + server, and request itself. + receive (starlette.types.Receive): A callable to receive the request, provided by the ASGI server. + send (starlette.types.Send): A callable to send the response, provided by the ASGI server. + """ + await self.stream_response(send) + + @abstractmethod + async def process_streaming_response(self, response: "ClientResponse"): + """ + Abstract method that must be implemented by the subclass. + This method should provide logic to handle the streaming response, such as parsing and accumulating data. + It is called as the response is being streamed from the network, and should be implemented to handle the + specific streaming data format and requirements of the subclass. + + Args: + response (aiohttp.ClientResponse): The response object to be processed, typically containing chunks of data. + """ + ... + + @abstractmethod + def extract_response_json(self, response: "ClientResponse") -> dict: + """ + Abstract method that must be implemented by the subclass. + This method should provide logic to extract JSON data from the response, including headers and content. + It is called after the response has been processed and is responsible for retrieving structured data that can be + used by the application. + + Args: + response (aiohttp.ClientResponse): The response object from which to extract JSON data. + """ + + def create_streaming_response( + self, token_streamer: Callable[[Send], Awaitable[None]] + ) -> "BTStreamingResponse": + """ + Creates a streaming response using the provided token streamer. + This method can be used by the subclass to create a response object that can be sent back to the client. + The token streamer should be implemented to generate the content of the response according to the specific + requirements of the subclass. + + Args: + token_streamer (Callable[[starlette.types.Send], Awaitable[None]]): A callable that takes a send function + and returns an awaitable. It's responsible for generating the content of the response. + + Returns: + BTStreamingResponse (bittensor.core.stream.StreamingSynapse.BTStreamingResponse): The streaming response + object, ready to be sent to the client. + """ + model_instance = BTStreamingResponseModel(token_streamer=token_streamer) + + return self.BTStreamingResponse(model_instance, synapse=self) diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py new file mode 100644 index 0000000000..435498038c --- /dev/null +++ b/bittensor/core/subtensor.py @@ -0,0 +1,4628 @@ +import copy +from datetime import datetime, timezone +from functools import lru_cache +from typing import TYPE_CHECKING, Any, Iterable, Optional, Union, cast + +import numpy as np +import scalecodec +from async_substrate_interface.errors import SubstrateRequestException +from async_substrate_interface.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 numpy.typing import NDArray + +from bittensor.core.async_subtensor import ProposalVoteData +from bittensor.core.axon import Axon +from bittensor.core.chain_data import ( + DelegatedInfo, + DelegateInfo, + DynamicInfo, + MetagraphInfo, + NeuronInfo, + NeuronInfoLite, + SelectiveMetagraphIndex, + StakeInfo, + SubnetInfo, + SubnetIdentity, + SubnetHyperparameters, + 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, +) +from bittensor.core.config import Config +from bittensor.core.errors import ChainError +from bittensor.core.extrinsics.children import ( + set_children_extrinsic, + root_set_pending_childkey_cooldown_extrinsic, +) +from bittensor.core.extrinsics.commit_reveal import commit_reveal_extrinsic +from bittensor.core.extrinsics.weights import ( + commit_weights_extrinsic, + reveal_weights_extrinsic, +) +from bittensor.core.extrinsics.liquidity import ( + add_liquidity_extrinsic, + modify_liquidity_extrinsic, + remove_liquidity_extrinsic, + toggle_user_liquidity_extrinsic, +) +from bittensor.core.extrinsics.move_stake import ( + transfer_stake_extrinsic, + swap_stake_extrinsic, + move_stake_extrinsic, +) +from bittensor.core.extrinsics.registration import ( + burned_register_extrinsic, + register_extrinsic, + register_subnet_extrinsic, + set_subnet_identity_extrinsic, +) +from bittensor.core.extrinsics.root import root_register_extrinsic +from bittensor.core.extrinsics.serving import ( + get_last_bonds_reset, + publish_metadata, + get_metadata, + serve_axon_extrinsic, +) +from bittensor.core.extrinsics.weights import set_weights_extrinsic +from bittensor.core.extrinsics.staking import ( + add_stake_extrinsic, + add_stake_multiple_extrinsic, +) +from bittensor.core.extrinsics.start_call import start_call_extrinsic +from bittensor.core.extrinsics.take import ( + decrease_take_extrinsic, + increase_take_extrinsic, +) +from bittensor.core.extrinsics.transfer import transfer_extrinsic +from bittensor.core.extrinsics.unstaking import ( + unstake_all_extrinsic, + unstake_extrinsic, + unstake_multiple_extrinsic, +) +from bittensor.core.metagraph import Metagraph +from bittensor.core.settings import ( + version_as_int, + SS58_FORMAT, + TYPE_REGISTRY, +) +from bittensor.core.types import ParamWithTypes, SubtensorMixin +from bittensor.utils import ( + Certificate, + decode_hex_identity_dict, + format_error_message, + is_valid_ss58_address, + torch, + u16_normalized_float, + u64_normalized_float, + deprecated_message, + get_transfer_fn_params, +) +from bittensor.utils.balance import ( + Balance, + fixed_to_float, + FixedPoint, + check_and_convert_to_balance, +) +from bittensor.utils.btlogging import logging +from bittensor.utils.liquidity import ( + calculate_fees, + get_fees, + tick_to_price, + price_to_tick, + LiquidityPosition, +) +from bittensor.utils.weight_utils import ( + generate_weight_hash, + U16_MAX, +) + +if TYPE_CHECKING: + from bittensor_wallet import Wallet + from async_substrate_interface.sync_substrate import QueryMapResult + from scalecodec.types import GenericCall + + +class Subtensor(SubtensorMixin): + """Thin layer for interacting with Substrate Interface. Mostly a collection of frequently used calls.""" + + def __init__( + self, + network: Optional[str] = None, + config: Optional[Config] = None, + log_verbose: bool = False, + fallback_endpoints: Optional[list[str]] = None, + retry_forever: bool = False, + _mock: bool = False, + archive_endpoints: Optional[list[str]] = None, + ): + """ + Initializes an instance of the Subtensor class. + + Arguments: + network: The network name or type to connect to. + config: Configuration object for the AsyncSubtensor instance. + log_verbose: Enables or disables verbose logging. + fallback_endpoints: List of fallback endpoints to use if default or provided network is not available. + Defaults to `None`. + retry_forever: Whether to retry forever on connection errors. Defaults to `False`. + _mock: Whether this is a mock instance. Mainly just for use in testing. + archive_endpoints: Similar to fallback_endpoints, but specifically only archive nodes. Will be used in cases + where you are requesting a block that is too old for your current (presumably lite) node. Defaults to + `None` + + Raises: + Any exceptions raised during the setup, configuration, or connection process. + """ + if config is None: + config = self.config() + self._config = copy.deepcopy(config) + self.chain_endpoint, self.network = self.setup_config(network, self._config) + + self.log_verbose = log_verbose + self._check_and_log_network_settings() + + logging.debug( + f"Connecting to network: [blue]{self.network}[/blue], " + f"chain_endpoint: [blue]{self.chain_endpoint}[/blue]> ..." + ) + self.substrate = self._get_substrate( + fallback_endpoints=fallback_endpoints, + retry_forever=retry_forever, + _mock=_mock, + archive_endpoints=archive_endpoints, + ) + if self.log_verbose: + logging.info( + f"Connected to {self.network} network and {self.chain_endpoint}." + ) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.close() + + def close(self): + """Closes the websocket connection.""" + self.substrate.close() + + def _get_substrate( + self, + fallback_endpoints: Optional[list[str]] = None, + retry_forever: bool = False, + _mock: bool = False, + archive_endpoints: Optional[list[str]] = None, + ) -> Union[SubstrateInterface, RetrySyncSubstrate]: + """Creates the Substrate instance based on provided arguments. + + Arguments: + fallback_endpoints: List of fallback chains endpoints to use if main network isn't available. Defaults to + `None`. + retry_forever: Whether to retry forever on connection errors. Defaults to `False`. + _mock: Whether this is a mock instance. Mainly just for use in testing. + archive_endpoints: Similar to fallback_endpoints, but specifically only archive nodes. Will be used in cases + where you are requesting a block that is too old for your current (presumably lite) node. Defaults to + `None` + + Returns: + the instance of the SubstrateInterface or RetrySyncSubstrate class. + """ + if fallback_endpoints or retry_forever or archive_endpoints: + return RetrySyncSubstrate( + url=self.chain_endpoint, + ss58_format=SS58_FORMAT, + type_registry=TYPE_REGISTRY, + use_remote_preset=True, + chain_name="Bittensor", + fallback_chains=fallback_endpoints, + retry_forever=retry_forever, + _mock=_mock, + archive_nodes=archive_endpoints, + ) + return SubstrateInterface( + url=self.chain_endpoint, + ss58_format=SS58_FORMAT, + type_registry=TYPE_REGISTRY, + use_remote_preset=True, + chain_name="Bittensor", + _mock=_mock, + ) + + # Subtensor queries =========================================================================================== + + def query_constant( + self, module_name: str, constant_name: str, block: Optional[int] = None + ) -> Optional["ScaleObj"]: + """ + Retrieves a constant from the specified module on the Bittensor blockchain. This function is used to access + fixed parameters or values defined within the blockchain's modules, which are essential for understanding + the network's configuration and rules. + + Args: + module_name: The name of the module containing the constant. + constant_name: The name of the constant to retrieve. + block: The blockchain block number at which to query the constant. + + Returns: + Optional[async_substrate_interface.types.ScaleObj]: The value of the constant if found, `None` otherwise. + + Constants queried through this function can include critical network parameters such as inflation rates, + consensus rules, or validation thresholds, providing a deeper understanding of the Bittensor network's + operational parameters. + """ + return self.substrate.get_constant( + module_name=module_name, + constant_name=constant_name, + block_hash=self.determine_block_hash(block), + ) + + def query_map( + self, + module: str, + name: str, + block: Optional[int] = None, + params: Optional[list] = None, + ) -> "QueryMapResult": + """ + Queries map storage from any module on the Bittensor blockchain. This function retrieves data structures that + represent key-value mappings, essential for accessing complex and structured data within the blockchain + modules. + + Args: + module: The name of the module from which to query the map storage. + name: The specific storage function within the module to query. + block: The blockchain block number at which to perform the query. + params: Parameters to be passed to the query. + + Returns: + result: A data structure representing the map storage if found, `None` otherwise. + + This function is particularly useful for retrieving detailed and structured data from various blockchain + modules, offering insights into the network's state and the relationships between its different components. + """ + result = self.substrate.query_map( + module=module, + storage_function=name, + params=params, + block_hash=self.determine_block_hash(block=block), + ) + return result + + def query_map_subtensor( + self, name: str, block: Optional[int] = None, params: Optional[list] = None + ) -> "QueryMapResult": + """ + Queries map storage from the Subtensor module on the Bittensor blockchain. This function is designed to retrieve + a map-like data structure, which can include various neuron-specific details or network-wide attributes. + + Args: + name: The name of the map storage function to query. + block: The blockchain block number at which to perform the query. + params: A list of parameters to pass to the query function. + + Returns: + An object containing the map-like data structure, or `None` if not found. + + This function is particularly useful for analyzing and understanding complex network structures and + relationships within the Bittensor ecosystem, such as interneuronal connections and stake distributions. + """ + return self.substrate.query_map( + module="SubtensorModule", + storage_function=name, + params=params, + block_hash=self.determine_block_hash(block), + ) + + def query_module( + self, + module: str, + name: str, + block: Optional[int] = None, + params: Optional[list] = None, + ) -> Optional[Union["ScaleObj", Any, FixedPoint]]: + """ + 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. + + Args: + module (str): The name of the module from which to query data. + name (str): The name of the storage function within the module. + block (Optional[int]): The blockchain block number at which to perform the query. + params (Optional[list[object]]): A list of parameters to pass to the query function. + + Returns: + An object containing the requested data if found, `None` otherwise. + + This versatile query function is key to accessing a wide range of data and insights from different parts of the + Bittensor blockchain, enhancing the understanding and analysis of the network's state and dynamics. + """ + return self.substrate.query( + module=module, + storage_function=name, + params=params, + block_hash=self.determine_block_hash(block), + ) + + def query_runtime_api( + self, + runtime_api: str, + method: str, + params: Optional[Union[list[Any], dict[str, Any]]] = None, + block: Optional[int] = None, + ) -> 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. This function is essential for advanced users who need to + interact with specific runtime methods and decode complex data types. + + Args: + runtime_api: The name of the runtime API to query. + method: The specific method within the runtime API to call. + params: The parameters to pass to the method call. + block: the block number for this query. + + Returns: + The Scale Bytes encoded result from the runtime API call, or `None` if the call fails. + + This function enables access to the deeper layers of the Bittensor blockchain, allowing for detailed and + specific interactions with the network's runtime environment. + """ + block_hash = self.determine_block_hash(block) + result = self.substrate.runtime_call(runtime_api, method, params, block_hash) + + return result.value + + def query_subtensor( + self, name: str, block: Optional[int] = None, params: Optional[list] = None + ) -> Optional[Union["ScaleObj", Any]]: + """ + Queries named storage from the Subtensor module on the Bittensor blockchain. This function is used to retrieve + specific data or parameters from the blockchain, such as stake, rank, or other neuron-specific attributes. + + Args: + name: The name of the storage function to query. + block: The blockchain block number at which to perform the query. + params: A list of parameters to pass to the query function. + + Returns: + query_response: An object containing the requested data. + + This query function is essential for accessing detailed information about the network and its neurons, providing + valuable insights into the state and dynamics of the Bittensor ecosystem. + """ + return self.substrate.query( + module="SubtensorModule", + storage_function=name, + params=params, + block_hash=self.determine_block_hash(block), + ) + + def state_call( + self, method: str, data: str, block: Optional[int] = None + ) -> dict[Any, Any]: + """ + Makes a state call to the Bittensor blockchain, allowing for direct queries of the blockchain's state. This + function is typically used for advanced queries that require specific method calls and data inputs. + + Args: + method: The method name for the state call. + data: The data to be passed to the method. + block: The blockchain block number at which to perform the state call. + + Returns: + result (dict[Any, Any]): The result of the rpc call. + + The state call function provides a more direct and flexible way of querying blockchain data, useful for specific + use cases where standard queries are insufficient. + """ + block_hash = self.determine_block_hash(block) + return self.substrate.rpc_request( + method="state_call", params=[method, data], block_hash=block_hash + ) + + # Common subtensor calls =========================================================================================== + + @property + def block(self) -> int: + return self.get_current_block() + + def all_subnets(self, block: Optional[int] = None) -> Optional[list["DynamicInfo"]]: + """ + Retrieves the subnet information for all subnets in the network. + + Args: + block (Optional[int]): The block number to query the subnet information from. + + Returns: + Optional[DynamicInfo]: A list of DynamicInfo objects, each containing detailed information about a subnet. + + """ + 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 = query.decode() + try: + subnet_prices = self.get_subnet_prices(block=block) + for sn in decoded: + sn.update( + {"price": subnet_prices.get(sn["netuid"], Balance.from_tao(0))} + ) + except (SubstrateRequestException, ValueError) as e: + logging.warning(f"Unable to fetch subnet prices for block {block}: {e}") + + return DynamicInfo.list_from_dicts(decoded) + + def blocks_since_last_step( + self, netuid: int, block: Optional[int] = None + ) -> Optional[int]: + """Returns number of blocks since the last epoch of the subnet. + + Arguments: + netuid (int): The unique identifier of the subnetwork. + block: the block number for this query. + + Returns: + block number of the last step in the subnet. + """ + query = self.query_subtensor( + name="BlocksSinceLastStep", block=block, params=[netuid] + ) + return query.value if query is not None and hasattr(query, "value") else query + + def blocks_since_last_update(self, netuid: int, uid: int) -> Optional[int]: + """ + Returns the number of blocks since the last update for a specific UID in the subnetwork. + + Arguments: + netuid (int): The unique identifier of the subnetwork. + uid (int): The unique identifier of the neuron. + + Returns: + Optional[int]: The number of blocks since the last update, or ``None`` if the subnetwork or UID does not + exist. + """ + call = self.get_hyperparameter(param_name="LastUpdate", netuid=netuid) + return None if not call else (self.get_current_block() - int(call[uid])) + + def bonds( + self, netuid: int, block: Optional[int] = None + ) -> list[tuple[int, list[tuple[int, int]]]]: + """ + Retrieves the bond distribution set by neurons within a specific subnet of the Bittensor network. + Bonds represent the investments or commitments made by neurons in one another, indicating a level of trust + and perceived value. This bonding mechanism is integral to the network's market-based approach to + measuring and rewarding machine intelligence. + + Args: + netuid: The network UID of the subnet to query. + block: the block number for this query. + + Returns: + List of tuples mapping each neuron's UID to its bonds with other neurons. + + Understanding bond distributions is crucial for analyzing the trust dynamics and market behavior within the + subnet. It reflects how neurons recognize and invest in each other's intelligence and contributions, + supporting diverse and niche systems within the Bittensor ecosystem. + """ + b_map_encoded = self.substrate.query_map( + module="SubtensorModule", + storage_function="Bonds", + params=[netuid], + 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)) + + return b_map + + def commit_reveal_enabled( + self, netuid: int, block: Optional[int] = None + ) -> Optional[bool]: + """ + Check if the commit-reveal mechanism is enabled for a given network at a specific block. + + Arguments: + netuid: The network identifier for which to check the commit-reveal mechanism. + block: The block number to query. + + Returns: + Returns the integer value of the hyperparameter if available; otherwise, returns None. + """ + call = self.get_hyperparameter( + param_name="CommitRevealWeightsEnabled", block=block, netuid=netuid + ) + return True if call is True else False + + def difficulty(self, netuid: int, block: Optional[int] = None) -> Optional[int]: + """ + Retrieves the 'Difficulty' hyperparameter for a specified subnet in the Bittensor network. + + This parameter is instrumental in determining the computational challenge required for neurons to participate in + consensus and validation processes. + + Arguments: + netuid: The unique identifier of the subnet. + block: The blockchain block number for the query. + + Returns: + Optional[int]: The value of the 'Difficulty' hyperparameter if the subnet exists, ``None`` otherwise. + + The 'Difficulty' parameter directly impacts the network's security and integrity by setting the computational + effort required for validating transactions and participating in the network's consensus mechanism. + """ + call = self.get_hyperparameter( + param_name="Difficulty", netuid=netuid, block=block + ) + if call is None: + return None + return int(call) + + def does_hotkey_exist(self, hotkey_ss58: str, block: Optional[int] = None) -> bool: + """ + Returns true if the hotkey is known by the chain and there are accounts. + + Args: + hotkey_ss58: The SS58 address of the hotkey. + block: the block number for this query. + + Returns: + `True` if the hotkey is known by the chain and there are accounts, `False` otherwise. + """ + result = 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 + + def get_all_subnets_info(self, block: Optional[int] = None) -> list["SubnetInfo"]: + """ + Retrieves detailed information about all subnets within the Bittensor network. This function provides + comprehensive data on each subnet, including its characteristics and operational parameters. + + Arguments: + block: The blockchain block number for the query. + + Returns: + list[SubnetInfo]: A list of SubnetInfo objects, each containing detailed information about a subnet. + + Gaining insights into the subnets' details assists in understanding the network's composition, the roles of + different subnets, and their unique features. + """ + result = self.query_runtime_api( + runtime_api="SubnetInfoRuntimeApi", + method="get_subnets_info_v2", + params=[], + block=block, + ) + if not result: + return [] + try: + subnets_prices = self.get_subnet_prices(block=block) + + for subnet in result: + subnet.update({"price": subnets_prices.get(subnet["netuid"], 0)}) + except (SubstrateRequestException, ValueError) as e: + logging.warning(f"Unable to fetch subnet prices for block {block}: {e}") + + return SubnetInfo.list_from_dicts(result) + + def get_balance(self, address: str, block: Optional[int] = None) -> Balance: + """ + Retrieves the balance for given coldkey. Always in TAO. + + Arguments: + address: coldkey address. + block (Optional[int]): The blockchain block number for the query. + + Returns: + Balance object in TAO. + """ + balance = self.substrate.query( + module="System", + storage_function="Account", + params=[address], + block_hash=self.determine_block_hash(block), + ) + return Balance(balance["data"]["free"]) + + def get_balances( + self, + *addresses: str, + block: Optional[int] = None, + ) -> dict[str, Balance]: + """ + Retrieves the balance for given coldkey(s) + + Arguments: + addresses (str): coldkey addresses(s). + block (Optional[int]): The blockchain block number for the query. + + Returns: + Dict of {address: Balance objects}. + """ + if not (block_hash := self.determine_block_hash(block)): + block_hash = self.substrate.get_chain_head() + calls = [ + ( + self.substrate.create_storage_key( + "System", "Account", [address], block_hash=block_hash + ) + ) + for address in addresses + ] + batch_call = self.substrate.query_multi(calls, block_hash=block_hash) + results = {} + for item in batch_call: + value = item[1] or {"data": {"free": 0}} + results.update({item[0].params[0]: Balance(value["data"]["free"])}) + return results + + def get_current_block(self) -> int: + """ + Returns the current block number on the Bittensor blockchain. This function provides the latest block number, + indicating the most recent state of the blockchain. + + Returns: + int: The current chain block number. + + Knowing the current block number is essential for querying real-time data and performing time-sensitive + operations on the blockchain. It serves as a reference point for network activities and data + synchronization. + """ + return self.substrate.get_block_number(None) + + @lru_cache(maxsize=128) + def _get_block_hash(self, block_id: int): + return self.substrate.get_block_hash(block_id) + + def get_block_hash(self, block: Optional[int] = None) -> str: + """ + Retrieves the hash of a specific block on the Bittensor blockchain. The block hash is a unique identifier + representing the cryptographic hash of the block's content, ensuring its integrity and immutability. + + Arguments: + block (int): The block number for which the hash is to be retrieved. + + Returns: + str: The cryptographic hash of the specified block. + + The block hash is a fundamental aspect of blockchain technology, providing a secure reference to each block's + data. It is crucial for verifying transactions, ensuring data consistency, and maintaining the + trustworthiness of the blockchain. + """ + if block is not None: + return self._get_block_hash(block) + else: + return self.substrate.get_chain_head() + + def determine_block_hash(self, block: Optional[int]) -> Optional[str]: + if block is None: + return None + else: + return self.get_block_hash(block=block) + + def encode_params( + self, + call_definition: dict[str, list["ParamWithTypes"]], + params: Union[list[Any], dict[str, Any]], + ) -> str: + """Returns a hex encoded string of the params using their types.""" + param_data = scalecodec.ScaleBytes(b"") + + for i, param in enumerate(call_definition["params"]): + scale_obj = self.substrate.create_scale_object(param["type"]) + if isinstance(params, list): + param_data += scale_obj.encode(params[i]) + else: + if param["name"] not in params: + raise ValueError(f"Missing param {param['name']} in params dict.") + + param_data += scale_obj.encode(params[param["name"]]) + + return param_data.to_hex() + + def get_hyperparameter( + self, param_name: str, netuid: int, block: Optional[int] = None + ) -> Optional[Any]: + """ + Retrieves a specified hyperparameter for a specific subnet. + + Arguments: + param_name (str): The name of the hyperparameter to retrieve. + netuid (int): The unique identifier of the subnet. + block: the block number at which to retrieve the hyperparameter. + + Returns: + The value of the specified hyperparameter if the subnet exists, or None + """ + block_hash = self.determine_block_hash(block) + if not self.subnet_exists(netuid, block=block): + logging.error(f"subnet {netuid} does not exist") + return None + + result = self.substrate.query( + module="SubtensorModule", + storage_function=param_name, + params=[netuid], + block_hash=block_hash, + ) + + return getattr(result, "value", result) + + def get_parents( + self, hotkey: str, netuid: int, block: Optional[int] = None + ) -> list[tuple[float, str]]: + """ + This method retrieves the parent of a given hotkey and netuid. It queries the SubtensorModule's ParentKeys + storage function to get the children and formats them before returning as a tuple. + + Arguments: + hotkey: The child hotkey SS58. + netuid: The netuid. + block: The block number for which the children are to be retrieved. + + Returns: + A list of formatted parents [(proportion, parent)] + """ + parents = self.substrate.query( + module="SubtensorModule", + storage_function="ParentKeys", + params=[hotkey, netuid], + block_hash=self.determine_block_hash(block), + ) + if parents: + formatted_parents = [] + for proportion, parent 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 + + return [] + + def get_children( + self, hotkey: str, netuid: int, block: Optional[int] = None + ) -> tuple[bool, list[tuple[float, str]], str]: + """ + This method retrieves the children of a given hotkey and netuid. It queries the SubtensorModule's ChildKeys + storage function to get the children and formats them before returning as a tuple. + + Arguments: + hotkey (str): The hotkey value. + netuid (int): The netuid value. + block (Optional[int]): The block number for which the children are to be retrieved. + + Returns: + A tuple containing a boolean indicating success or failure, a list of formatted children, and an error + message (if applicable) + """ + try: + children = self.substrate.query( + module="SubtensorModule", + storage_function="ChildKeys", + params=[hotkey, netuid], + block_hash=self.determine_block_hash(block), + ) + if children: + formatted_children = [] + for proportion, 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, "" + else: + return True, [], "" + except SubstrateRequestException as e: + return False, [], format_error_message(e) + + def get_children_pending( + self, + hotkey: str, + netuid: int, + block: Optional[int] = None, + ) -> tuple[ + list[tuple[float, str]], + int, + ]: + """ + This method retrieves the pending children of a given hotkey and netuid. + It queries the SubtensorModule's PendingChildKeys storage function. + + Arguments: + hotkey (str): The hotkey value. + netuid (int): The netuid value. + block (Optional[int]): The block number for which the children are to be retrieved. + + Returns: + list[tuple[float, str]]: A list of children with their proportions. + int: The cool-down block number. + """ + + children, cooldown = self.substrate.query( + module="SubtensorModule", + storage_function="PendingChildKeys", + params=[netuid, hotkey], + block_hash=self.determine_block_hash(block), + ).value + + return ( + [ + ( + u64_normalized_float(proportion), + decode_account_id(child[0]), + ) + for proportion, child in children + ], + cooldown, + ) + + def get_commitment(self, netuid: int, uid: int, block: Optional[int] = None) -> str: + """ + Retrieves the on-chain commitment for a specific neuron in the Bittensor network. + + Arguments: + netuid (int): The unique identifier of the subnetwork. + uid (int): The unique identifier of the neuron. + block (Optional[int]): The block number to retrieve the commitment from. If None, the latest block is used. + Default is ``None``. + + Returns: + str: The commitment data as a string. + """ + metagraph = self.metagraph(netuid) + try: + hotkey = metagraph.hotkeys[uid] # type: ignore + except IndexError: + logging.error( + "Your uid is not in the hotkeys. Please double-check your UID." + ) + return "" + + metadata = cast(dict, get_metadata(self, netuid, hotkey, block)) + try: + return decode_metadata(metadata) + + except TypeError: + return "" + + def get_last_commitment_bonds_reset_block( + self, netuid: int, uid: int + ) -> Optional[int]: + """ + Retrieves the last block number when the bonds reset were triggered by publish_metadata for a specific neuron. + + Arguments: + netuid (int): The unique identifier of the subnetwork. + uid (int): The unique identifier of the neuron. + + Returns: + Optional[int]: The block number when the bonds were last reset, or None if not found. + """ + + metagraph = self.metagraph(netuid) + try: + hotkey = metagraph.hotkeys[uid] + except IndexError: + logging.error( + "Your uid is not in the hotkeys. Please double-check your UID." + ) + return None + block = get_last_bonds_reset(self, netuid, hotkey) + if block is None: + return None + return decode_block(block) + + def get_all_commitments( + self, netuid: int, block: Optional[int] = None + ) -> dict[str, str]: + query = self.query_map( + module="Commitments", + name="CommitmentOf", + params=[netuid], + block=block, + ) + result = {} + for id_, value in query: + result[decode_account_id(id_[0])] = decode_metadata(value) + return result + + def get_revealed_commitment_by_hotkey( + self, + netuid: int, + hotkey_ss58_address: str, + block: Optional[int] = None, + ) -> Optional[tuple[tuple[int, str], ...]]: + """Returns hotkey related revealed commitment for a given netuid. + + Arguments: + netuid (int): The unique identifier of the subnetwork. + hotkey_ss58_address (str): The ss58 address of the committee member. + block (Optional[int]): The block number to retrieve the commitment from. Default is ``None``. + + Returns: + result (tuple[int, str): A tuple of reveal block and commitment message. + """ + if not is_valid_ss58_address(address=hotkey_ss58_address): + raise ValueError(f"Invalid ss58 address {hotkey_ss58_address} provided.") + + query = self.query_module( + module="Commitments", + name="RevealedCommitments", + params=[netuid, hotkey_ss58_address], + block=block, + ) + if query is None: + return None + return tuple(decode_revealed_commitment(pair) for pair in query) + + def get_revealed_commitment( + self, + netuid: int, + uid: int, + block: Optional[int] = None, + ) -> Optional[tuple[tuple[int, str], ...]]: + """Returns uid related revealed commitment for a given netuid. + + Arguments: + netuid (int): The unique identifier of the subnetwork. + uid (int): The neuron uid to retrieve the commitment from. + block (Optional[int]): The block number to retrieve the commitment from. Default is ``None``. + + Returns: + result (Optional[tuple[int, str]]: A tuple of reveal block and commitment message. + + Example of result: + ( (12, "Alice message 1"), (152, "Alice message 2") ) + ( (12, "Bob message 1"), (147, "Bob message 2") ) + """ + try: + meta_info = self.get_metagraph_info(netuid, block=block) + if meta_info: + hotkey_ss58_address = meta_info.hotkeys[uid] + else: + raise ValueError(f"Subnet with netuid {netuid} does not exist.") + except IndexError: + raise ValueError(f"Subnet {netuid} does not have a neuron with uid {uid}.") + + return self.get_revealed_commitment_by_hotkey( + netuid=netuid, hotkey_ss58_address=hotkey_ss58_address, block=block + ) + + def get_all_revealed_commitments( + self, netuid: int, block: Optional[int] = None + ) -> dict[str, tuple[tuple[int, str], ...]]: + """Returns all revealed commitments for a given netuid. + + Arguments: + netuid (int): The unique identifier of the subnetwork. + block (Optional[int]): The block number to retrieve the commitment from. Default is ``None``. + + Returns: + result (dict): A dictionary of all revealed commitments in view + {ss58_address: (reveal block, commitment message)}. + + Example of result: + { + "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY": ( (12, "Alice message 1"), (152, "Alice message 2") ), + "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty": ( (12, "Bob message 1"), (147, "Bob message 2") ), + } + """ + query = self.query_map( + module="Commitments", + name="RevealedCommitments", + params=[netuid], + block=block, + ) + + result = {} + for pair in query: + hotkey_ss58_address, commitment_message = ( + decode_revealed_commitment_with_hotkey(pair) + ) + result[hotkey_ss58_address] = commitment_message + return result + + def get_current_weight_commit_info( + self, netuid: int, block: Optional[int] = None + ) -> list[tuple[str, str, int]]: + """ + Retrieves CRV3 weight commit information for a specific subnet. + + Arguments: + netuid (int): The unique identifier of the subnet. + block (Optional[int]): The blockchain block number for the query. Default is ``None``. + + Returns: + A list of commit details, where each item contains: + - ss58_address: The address of the committer. + - commit_message: The commit message. + - reveal_round: The round when the commitment was revealed. + + The list may be empty if there are no commits found. + + """ + deprecated_message( + message="The method `get_current_weight_commit_info` is deprecated and will be removed in version 10.0.0. " + "Use `get_current_weight_commit_info_v2` instead." + ) + result = self.substrate.query_map( + module="SubtensorModule", + storage_function="CRV3WeightCommits", + params=[netuid], + block_hash=self.determine_block_hash(block), + ) + + commits = result.records[0][1] if result.records else [] + return [WeightCommitInfo.from_vec_u8(commit) for commit in commits] + + def get_current_weight_commit_info_v2( + self, netuid: int, block: Optional[int] = None + ) -> list[tuple[str, int, str, int]]: + """ + Retrieves CRV3 weight commit information for a specific subnet. + + Arguments: + netuid (int): The unique identifier of the subnet. + block (Optional[int]): The blockchain block number for the query. Default is ``None``. + + Returns: + A list of commit details, where each item contains: + - ss58_address: The address of the committer. + - commit_block: The block number when the commitment was made. + - commit_message: The commit message. + - reveal_round: The round when the commitment was revealed. + + The list may be empty if there are no commits found. + """ + result = self.substrate.query_map( + module="SubtensorModule", + storage_function="CRV3WeightCommitsV2", + params=[netuid], + block_hash=self.determine_block_hash(block), + ) + + commits = result.records[0][1] if result.records else [] + return [WeightCommitInfo.from_vec_u8_v2(commit) for commit in commits] + + def get_delegate_by_hotkey( + self, hotkey_ss58: str, block: Optional[int] = None + ) -> Optional["DelegateInfo"]: + """ + Retrieves detailed information about a delegate neuron based on its hotkey. This function provides a + comprehensive view of the delegate's status, including its stakes, nominators, and reward distribution. + + Arguments: + hotkey_ss58 (str): The ``SS58`` address of the delegate's hotkey. + block (Optional[int]): The blockchain block number for the query. + + Returns: + Optional[DelegateInfo]: Detailed information about the delegate neuron, ``None`` if not found. + + This function is essential for understanding the roles and influence of delegate neurons within the Bittensor + network's consensus and governance structures. + """ + + result = self.query_runtime_api( + runtime_api="DelegateInfoRuntimeApi", + method="get_delegate", + params=[hotkey_ss58], + block=block, + ) + + if not result: + return None + + return DelegateInfo.from_dict(result) + + def get_delegate_identities( + self, block: Optional[int] = None + ) -> dict[str, ChainIdentity]: + """ + Fetches delegates identities from the chain. + + Arguments: + block (Optional[int]): The blockchain block number for the query. + + Returns: + Dict {ss58: ChainIdentity, ...} + + """ + identities = self.substrate.query_map( + module="SubtensorModule", + storage_function="IdentitiesV2", + block_hash=self.determine_block_hash(block), + ) + + return { + decode_account_id(ss58_address[0]): ChainIdentity.from_dict( + decode_hex_identity_dict(identity.value), + ) + for ss58_address, identity in identities + } + + def get_delegate_take(self, hotkey_ss58: str, block: Optional[int] = None) -> float: + """ + Retrieves the delegate 'take' percentage for a neuron identified by its hotkey. The 'take' represents the + percentage of rewards that the delegate claims from its nominators' stakes. + + Arguments: + hotkey_ss58 (str): The ``SS58`` address of the neuron's hotkey. + block (Optional[int]): The blockchain block number for the query. + + Returns: + float: The delegate take percentage. + + The delegate take is a critical parameter in the network's incentive structure, influencing the distribution of + rewards among neurons and their nominators. + """ + result = self.query_subtensor( + name="Delegates", + block=block, + params=[hotkey_ss58], + ) + + return u16_normalized_float(result.value) # type: ignore + + def get_delegated( + self, coldkey_ss58: str, block: Optional[int] = None + ) -> list[DelegatedInfo]: + """ + Retrieves a list of delegates and their associated stakes for a given coldkey. This function identifies the + delegates that a specific account has staked tokens on. + + Arguments: + coldkey_ss58 (str): The `SS58` address of the account's coldkey. + block (Optional[int]): The blockchain block number for the query. + + Returns: + A list containing the delegated information for the specified coldkey. + + This function is important for account holders to understand their stake allocations and their involvement in + the network's delegation and consensus mechanisms. + """ + + result = self.query_runtime_api( + runtime_api="DelegateInfoRuntimeApi", + method="get_delegated", + params=[coldkey_ss58], + block=block, + ) + + if not result: + return [] + + return DelegatedInfo.list_from_dicts(result) + + def get_delegates(self, block: Optional[int] = None) -> list["DelegateInfo"]: + """ + Fetches all delegates on the chain + + Arguments: + block (Optional[int]): The blockchain block number for the query. + + Returns: + List of DelegateInfo objects, or an empty list if there are no delegates. + """ + result = self.query_runtime_api( + runtime_api="DelegateInfoRuntimeApi", + method="get_delegates", + params=[], + block=block, + ) + if result: + return DelegateInfo.list_from_dicts(result) + else: + return [] + + def get_existential_deposit(self, block: Optional[int] = None) -> Optional[Balance]: + """ + Retrieves the existential deposit amount for the Bittensor blockchain. Always in TAO. + The existential deposit is the minimum amount of TAO required for an account to exist on the blockchain. + Accounts with balances below this threshold can be reaped to conserve network resources. + + Arguments: + block (Optional[int]): The blockchain block number for the query. + + Returns: + The existential deposit amount. Always in TAO. + + The existential deposit is a fundamental economic parameter in the Bittensor network, ensuring efficient use of + storage and preventing the proliferation of dust accounts. + """ + result = self.substrate.get_constant( + module_name="Balances", + constant_name="ExistentialDeposit", + block_hash=self.determine_block_hash(block), + ) + + if result is None: + raise Exception("Unable to retrieve existential deposit amount.") + + return Balance.from_rao(getattr(result, "value", 0)) + + def get_hotkey_owner( + self, hotkey_ss58: str, block: Optional[int] = None + ) -> Optional[str]: + """ + Retrieves the owner of the given hotkey at a specific block hash. + This function queries the blockchain for the owner of the provided hotkey. If the hotkey does not exist at the + specified block hash, it returns None. + + Arguments: + hotkey_ss58 (str): The SS58 address of the hotkey. + block (Optional[int]): The blockchain block number for the query. + + Returns: + Optional[str]: The SS58 address of the owner if the hotkey exists, or None if it doesn't. + """ + hk_owner_query = self.substrate.query( + module="SubtensorModule", + storage_function="Owner", + params=[hotkey_ss58], + block_hash=self.determine_block_hash(block), + ) + exists = False + if hk_owner_query: + exists = self.does_hotkey_exist(hotkey_ss58, block=block) + hotkey_owner = hk_owner_query if exists else None + return hotkey_owner + + def get_minimum_required_stake(self) -> Balance: + """ + Returns the minimum required stake for nominators in the Subtensor network. + + Returns: + The minimum required stake as a Balance object in TAO. + """ + result = self.substrate.query( + module="SubtensorModule", storage_function="NominatorMinRequiredStake" + ) + + return Balance.from_rao(getattr(result, "value", 0)) + + def get_metagraph_info( + self, + netuid: int, + field_indices: Optional[Union[list[SelectiveMetagraphIndex], list[int]]] = None, + block: Optional[int] = None, + ) -> Optional[MetagraphInfo]: + """ + Retrieves full or partial metagraph information for the specified subnet (netuid). + + Arguments: + netuid: The NetUID of the subnet to query. + field_indices: An optional list of SelectiveMetagraphIndex or int values specifying which fields to retrieve. + If not provided, all available fields will be returned. + block: The block number at which to query the data. If not specified, the current block or one determined + via reuse_block or block_hash will be used. + + Returns: + Optional[MetagraphInfo]: A MetagraphInfo object containing the requested subnet data, or None if the subnet + with the given netuid does not exist. + + Example: + meta_info = subtensor.get_metagraph_info(netuid=2) + + partial_meta_info = subtensor.get_metagraph_info( + netuid=2, + field_indices=[SelectiveMetagraphIndex.Name, SelectiveMetagraphIndex.OwnerHotkeys] + ) + """ + block_hash = self.determine_block_hash(block) + + if field_indices: + if isinstance(field_indices, list) and all( + isinstance(f, (SelectiveMetagraphIndex, int)) for f in field_indices + ): + indexes = [ + f.value if isinstance(f, SelectiveMetagraphIndex) else f + for f in field_indices + ] + else: + raise ValueError( + "`field_indices` must be a list of SelectiveMetagraphIndex enums or ints." + ) + + query = self.substrate.runtime_call( + "SubnetInfoRuntimeApi", + "get_selective_metagraph", + params=[netuid, indexes if 0 in indexes else [0] + indexes], + block_hash=block_hash, + ) + else: + query = self.substrate.runtime_call( + "SubnetInfoRuntimeApi", + "get_metagraph", + params=[netuid], + block_hash=block_hash, + ) + + if query.value is None: + logging.error(f"Subnet {netuid} does not exist.") + return None + + return MetagraphInfo.from_dict(query.value) + + def get_all_metagraphs_info( + self, block: Optional[int] = None + ) -> list[MetagraphInfo]: + """ + Retrieves a list of MetagraphInfo objects for all subnets + + Arguments: + block: the block number at which to retrieve the hyperparameter. Do not specify if using block_hash or + reuse_block + + Returns: + MetagraphInfo dataclass + """ + block_hash = self.determine_block_hash(block) + query = self.substrate.runtime_call( + "SubnetInfoRuntimeApi", + "get_all_metagraphs", + block_hash=block_hash, + ) + return MetagraphInfo.list_from_dicts(query.value) + + def get_netuids_for_hotkey( + self, hotkey_ss58: str, block: Optional[int] = None + ) -> list[int]: + """ + Retrieves a list of subnet UIDs (netuids) for which a given hotkey is a member. This function identifies the + specific subnets within the Bittensor network where the neuron associated with the hotkey is active. + + Arguments: + hotkey_ss58 (str): The ``SS58`` address of the neuron's hotkey. + block (Optional[int]): The blockchain block number for the query. + + Returns: + A list of netuids where the neuron is a member. + """ + result = self.substrate.query_map( + module="SubtensorModule", + storage_function="IsNetworkMember", + params=[hotkey_ss58], + block_hash=self.determine_block_hash(block), + ) + netuids = [] + if result.records: + for record in result: + if record[1].value: + netuids.append(record[0]) + return netuids + + def get_neuron_certificate( + self, hotkey: str, netuid: int, block: Optional[int] = None + ) -> Optional[Certificate]: + """ + Retrieves the TLS certificate for a specific neuron identified by its unique identifier (UID) within a + specified subnet (netuid) of the Bittensor network. + + Arguments: + hotkey: The hotkey to query. + netuid: The unique identifier of the subnet. + block: The blockchain block number for the query. + + Returns: + the certificate of the neuron if found, `None` otherwise. + + 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], + ) + try: + if certificate_query: + certificate = cast(dict, certificate_query) + return Certificate(certificate) + except AttributeError: + return None + return None + + def get_all_neuron_certificates( + self, netuid: int, block: Optional[int] = None + ) -> dict[str, Certificate]: + """ + Retrieves the TLS certificates for neurons within a specified subnet (netuid) of the Bittensor network. + + Arguments: + netuid: The unique identifier of the subnet. + block: The blockchain block number for the query. + + Returns: + {ss58: Certificate} for the key/Certificate pairs on the subnet + + This function is used for certificate discovery for setting up mutual tls communication between neurons. + """ + query_certificates = self.query_map( + module="SubtensorModule", + name="NeuronCertificates", + params=[netuid], + block=block, + ) + output = {} + for key, item in query_certificates: + output[decode_account_id(key)] = Certificate(item.value) + return output + + def get_liquidity_list( + self, + wallet: "Wallet", + netuid: int, + block: Optional[int] = None, + ) -> Optional[list[LiquidityPosition]]: + """ + Retrieves all liquidity positions for the given wallet on a specified subnet (netuid). + Calculates associated fee rewards based on current global and tick-level fee data. + + Args: + wallet: Wallet instance to fetch positions for. + netuid: Subnet unique id. + block: The blockchain block number for the query. + + Returns: + List of liquidity positions, or None if subnet does not exist. + """ + if not self.subnet_exists(netuid=netuid): + logging.debug(f"Subnet {netuid} does not exist.") + return None + + if not self.is_subnet_active(netuid=netuid): + logging.debug(f"Subnet {netuid} is not active.") + return None + + block_hash = self.determine_block_hash(block) + + # Fetch global fees and current price + fee_global_tao_query_sk = self.substrate.create_storage_key( + pallet="Swap", + storage_function="FeeGlobalTao", + params=[netuid], + block_hash=block_hash, + ) + fee_global_alpha_query_sk = self.substrate.create_storage_key( + pallet="Swap", + storage_function="FeeGlobalAlpha", + params=[netuid], + block_hash=block_hash, + ) + sqrt_price_query_sk = self.substrate.create_storage_key( + pallet="Swap", + storage_function="AlphaSqrtPrice", + params=[netuid], + block_hash=block_hash, + ) + fee_global_tao_query, fee_global_alpha_query, sqrt_price_query = ( + self.substrate.query_multi( + [ + fee_global_tao_query_sk, + fee_global_alpha_query_sk, + sqrt_price_query_sk, + ], + block_hash=block_hash, + ) + ) + + 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]) + current_tick = price_to_tick(sqrt_price**2) + + # Fetch positions + positions_response = self.query_map( + module="Swap", + name="Positions", + block=block, + params=[netuid, wallet.coldkeypub.ss58_address], + ) + positions_values: list[tuple[dict, 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] + + tick_low_sk = self.substrate.create_storage_key( + pallet="Swap", + storage_function="Ticks", + params=[netuid, tick_low_idx], + block_hash=block_hash, + ) + tick_high_sk = self.substrate.create_storage_key( + pallet="Swap", + storage_function="Ticks", + params=[netuid, tick_high_idx], + block_hash=block_hash, + ) + positions_values.append((position, tick_low_idx, tick_high_idx)) + positions_storage_keys.extend([tick_low_sk, tick_high_sk]) + # query all our ticks at once + ticks_query = self.substrate.query_multi( + positions_storage_keys, block_hash=block_hash + ) + # iterator with just the values + ticks = iter([x[1] for x in ticks_query]) + positions = [] + for position, tick_low_idx, tick_high_idx in positions_values: + tick_low = next(ticks) + tick_high = next(ticks) + + # Calculate fees above/below range for both tokens + tao_below = get_fees( + current_tick=current_tick, + tick=tick_low, + tick_index=tick_low_idx, + quote=True, + global_fees_tao=fee_global_tao, + global_fees_alpha=fee_global_alpha, + above=False, + ) + tao_above = get_fees( + current_tick=current_tick, + tick=tick_high, + tick_index=tick_high_idx, + quote=True, + global_fees_tao=fee_global_tao, + global_fees_alpha=fee_global_alpha, + above=True, + ) + alpha_below = get_fees( + current_tick=current_tick, + tick=tick_low, + tick_index=tick_low_idx, + quote=False, + global_fees_tao=fee_global_tao, + global_fees_alpha=fee_global_alpha, + above=False, + ) + alpha_above = get_fees( + current_tick=current_tick, + tick=tick_high, + tick_index=tick_high_idx, + quote=False, + global_fees_tao=fee_global_tao, + global_fees_alpha=fee_global_alpha, + above=True, + ) + + # Calculate fees earned by position + fees_tao, fees_alpha = calculate_fees( + position=position, + global_fees_tao=fee_global_tao, + global_fees_alpha=fee_global_alpha, + tao_fees_below_low=tao_below, + tao_fees_above_high=tao_above, + alpha_fees_below_low=alpha_below, + alpha_fees_above_high=alpha_above, + netuid=netuid, + ) + + 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"), + } + ) + ) + + return positions + + def get_neuron_for_pubkey_and_subnet( + self, hotkey_ss58: str, netuid: int, block: Optional[int] = None + ) -> Optional["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 + network. + + Arguments: + hotkey_ss58 (str): The ``SS58`` address of the neuron's hotkey. + netuid (int): The unique identifier of the subnet. + block (Optional[int]): The blockchain block number for the query. + + Returns: + Optional[bittensor.core.chain_data.neuron_info.NeuronInfo]: Detailed information about the neuron if found, + ``None`` otherwise. + + This function is crucial for accessing specific neuron data and understanding its status, stake, and other + attributes within a particular subnet of the Bittensor ecosystem. + """ + block_hash = self.determine_block_hash(block) + uid = self.substrate.query( + module="SubtensorModule", + storage_function="Uids", + params=[netuid, hotkey_ss58], + block_hash=block_hash, + ) + if uid is None: + return NeuronInfo.get_null_neuron() + + result = self.query_runtime_api( + runtime_api="NeuronInfoRuntimeApi", + method="get_neuron", + params=[netuid, uid.value], + block=block, + ) + + if not result: + return NeuronInfo.get_null_neuron() + + return NeuronInfo.from_dict(result) + + def get_next_epoch_start_block( + self, netuid: int, block: Optional[int] = None + ) -> Optional[int]: + """ + Calculates the first block number of the next epoch for the given subnet. + + If `block` is not provided, the current chain block will be used. Epochs are + determined based on the subnet's tempo (i.e., blocks per epoch). The result + is the block number at which the next epoch will begin. + + Args: + netuid (int): The unique identifier of the subnet. + block (Optional[int], optional): The reference block to calculate from. + If None, uses the current chain block height. + + Returns: + int: The block number at which the next epoch will start. + """ + block = block or self.block + blocks_since_last_step = self.blocks_since_last_step(netuid=netuid, block=block) + tempo = self.tempo(netuid=netuid, block=block) + + if block and blocks_since_last_step is not None and tempo: + return block - blocks_since_last_step + tempo + 1 + return None + + def get_owned_hotkeys( + self, + coldkey_ss58: str, + block: Optional[int] = None, + reuse_block: bool = False, + ) -> list[str]: + """ + Retrieves all hotkeys owned by a specific coldkey address. + + Args: + coldkey_ss58 (str): The SS58 address of the coldkey to query. + block (int): The blockchain block number for the query. + reuse_block (bool): Whether to reuse the last-used blockchain block hash. + + Returns: + list[str]: A list of hotkey SS58 addresses owned by the coldkey. + """ + block_hash = self.determine_block_hash(block) + owned_hotkeys = self.substrate.query( + module="SubtensorModule", + storage_function="OwnedHotkeys", + params=[coldkey_ss58], + block_hash=block_hash, + reuse_block_hash=reuse_block, + ) + return [decode_account_id(hotkey[0]) for hotkey in owned_hotkeys or []] + + def get_stake( + self, + coldkey_ss58: str, + hotkey_ss58: str, + netuid: int, + block: Optional[int] = None, + ) -> Balance: + """ + Returns the amount of Alpha staked by a specific coldkey to a specific hotkey within a given subnet. + This function retrieves the delegated stake balance, referred to as the 'Alpha' value. + + Args: + coldkey_ss58: The SS58 address of the coldkey that delegated the stake. This address owns the stake. + hotkey_ss58: The ss58 address of the hotkey which the stake is on. + netuid: The unique identifier of the subnet to query. + block: The specific block number at which to retrieve the stake information. If None, the current stake at + the latest block is returned. Defaults to ``None``. + + Returns: + An object representing the amount of Alpha (TAO ONLY if the subnet's netuid is 0) currently staked from the + specified coldkey to the specified hotkey within the given subnet. + """ + alpha_shares_query = self.query_module( + module="SubtensorModule", + name="Alpha", + block=block, + params=[hotkey_ss58, coldkey_ss58, netuid], + ) + alpha_shares = cast(FixedPoint, alpha_shares_query) + + hotkey_alpha_obj: ScaleObj = self.query_module( + module="SubtensorModule", + name="TotalHotkeyAlpha", + block=block, + params=[hotkey_ss58, netuid], + ) + hotkey_alpha = hotkey_alpha_obj.value + + hotkey_shares_query = self.query_module( + module="SubtensorModule", + name="TotalHotkeyShares", + block=block, + params=[hotkey_ss58, netuid], + ) + hotkey_shares = cast(FixedPoint, hotkey_shares_query) + + alpha_shares_as_float = fixed_to_float(alpha_shares) + hotkey_shares_as_float = fixed_to_float(hotkey_shares) + + if hotkey_shares_as_float == 0: + return Balance.from_rao(0).set_unit(netuid=netuid) + + stake = alpha_shares_as_float / hotkey_shares_as_float * hotkey_alpha + + return Balance.from_rao(int(stake)).set_unit(netuid=netuid) + + # TODO: remove unused parameters in SDK.v10 + def get_stake_add_fee( + self, + amount: Balance, + netuid: int, + coldkey_ss58: str, + hotkey_ss58: str, + block: Optional[int] = None, + ) -> Balance: + """ + Calculates the fee for adding new stake to a hotkey. + + Args: + amount: Amount of stake to add in TAO + netuid: Netuid of subnet + coldkey_ss58: SS58 address of coldkey + hotkey_ss58: SS58 address of hotkey + block: Block number at which to perform the calculation + + Returns: + The calculated stake fee as a Balance object + """ + return self.get_stake_operations_fee(amount=amount, netuid=netuid, block=block) + + def get_subnet_info( + self, netuid: int, block: Optional[int] = None + ) -> Optional["SubnetInfo"]: + """ + Retrieves detailed information about subnet within the Bittensor network. + This function provides comprehensive data on subnet, including its characteristics and operational parameters. + + Arguments: + netuid: The unique identifier of the subnet. + block: The blockchain block number for the query. + + Returns: + SubnetInfo: A SubnetInfo objects, each containing detailed information about a subnet. + + Gaining insights into the subnet's details assists in understanding the network's composition, the roles of + different subnets, and their unique features. + """ + result = self.query_runtime_api( + runtime_api="SubnetInfoRuntimeApi", + method="get_subnet_info_v2", + params=[netuid], + block=block, + ) + if not result: + return None + return SubnetInfo.from_dict(result) + + def get_subnet_price( + self, + netuid: int, + block: Optional[int] = None, + ) -> Balance: + """Gets the current Alpha price in TAO for all subnets. + + Arguments: + netuid: The unique identifier of the subnet. + block: The blockchain block number for the query. + + Returns: + The current Alpha price in TAO units for the specified subnet. + """ + # SN0 price is always 1 TAO + if netuid == 0: + return Balance.from_tao(1) + + block_hash = self.determine_block_hash(block=block) + current_sqrt_price = self.substrate.query( + module="Swap", + storage_function="AlphaSqrtPrice", + params=[netuid], + block_hash=block_hash, + ) + + current_sqrt_price = fixed_to_float(current_sqrt_price) + current_price = current_sqrt_price * current_sqrt_price + return Balance.from_rao(int(current_price * 1e9)) + + def get_subnet_prices( + self, + block: Optional[int] = None, + ) -> dict[int, Balance]: + """Gets the current Alpha price in TAO for a specified subnet. + + Args: + block: The blockchain block number for the query. Default to `None`. + + Returns: + dict: + - subnet unique ID + - The current Alpha price in TAO units for the specified subnet. + """ + block_hash = self.determine_block_hash(block=block) + + current_sqrt_prices = self.substrate.query_map( + module="Swap", + storage_function="AlphaSqrtPrice", + block_hash=block_hash, + page_size=129, # total number of subnets + ) + + prices = {} + for id_, current_sqrt_price in current_sqrt_prices: + current_sqrt_price = fixed_to_float(current_sqrt_price) + current_price = current_sqrt_price * current_sqrt_price + current_price_in_tao = Balance.from_rao(int(current_price * 1e9)) + prices.update({id_: current_price_in_tao}) + + # SN0 price is always 1 TAO + prices.update({0: Balance.from_tao(1)}) + return prices + + def get_timelocked_weight_commits( + self, netuid: int, block: Optional[int] = None + ) -> list[tuple[str, int, str, int]]: + """ + Retrieves CRv4 weight commit information for a specific subnet. + + Arguments: + netuid (int): The unique identifier of the subnet. + block (Optional[int]): The blockchain block number for the query. Default is ``None``. + + Returns: + A list of commit details, where each item contains: + - ss58_address: The address of the committer. + - commit_block: The block number when the commitment was made. + - commit_message: The commit message. + - reveal_round: The round when the commitment was revealed. + + The list may be empty if there are no commits found. + """ + result = self.substrate.query_map( + module="SubtensorModule", + storage_function="TimelockedWeightCommits", + params=[netuid], + block_hash=self.determine_block_hash(block=block), + ) + + commits = result.records[0][1] if result.records else [] + return [WeightCommitInfo.from_vec_u8_v2(commit) for commit in commits] + + # TODO: remove unused parameters in SDK.v10 + def get_unstake_fee( + self, + amount: Balance, + netuid: int, + coldkey_ss58: str, + hotkey_ss58: str, + block: Optional[int] = None, + ) -> Balance: + """ + Calculates the fee for unstaking from a hotkey. + + Args: + amount: Amount of stake to unstake in TAO + netuid: Netuid of subnet + coldkey_ss58: SS58 address of coldkey + hotkey_ss58: SS58 address of hotkey + block: Block number at which to perform the calculation + + Returns: + The calculated stake fee as a Balance object + """ + return self.get_stake_operations_fee(amount=amount, netuid=netuid, block=block) + + # TODO: remove unused parameters in SDK.v10 + def get_stake_movement_fee( + self, + amount: Balance, + origin_netuid: int, + origin_hotkey_ss58: str, + origin_coldkey_ss58: str, + destination_netuid: int, + destination_hotkey_ss58: str, + destination_coldkey_ss58: str, + block: Optional[int] = None, + ) -> Balance: + """ + Calculates the fee for moving stake between hotkeys/subnets/coldkeys. + + Args: + amount: Amount of stake to move in TAO + origin_netuid: Netuid of origin subnet + origin_hotkey_ss58: SS58 address of origin hotkey + origin_coldkey_ss58: SS58 address of origin coldkey + destination_netuid: Netuid of destination subnet + destination_hotkey_ss58: SS58 address of destination hotkey + destination_coldkey_ss58: SS58 address of destination coldkey + block: Block number at which to perform the calculation + + Returns: + The calculated stake fee as a Balance object + """ + return self.get_stake_operations_fee( + amount=amount, netuid=origin_netuid, block=block + ) + + def get_stake_for_coldkey_and_hotkey( + self, + coldkey_ss58: str, + hotkey_ss58: str, + netuids: Optional[list[int]] = None, + block: Optional[int] = None, + ) -> dict[int, StakeInfo]: + """ + Retrieves all coldkey-hotkey pairing stake across specified (or all) subnets + + Arguments: + coldkey_ss58 (str): The SS58 address of the coldkey. + hotkey_ss58 (str): The SS58 address of the hotkey. + netuids (Optional[list[int]]): The subnet IDs to query for. Set to `None` for all subnets. + block (Optional[int]): The block number at which to query the stake information. + + Returns: + A {netuid: StakeInfo} pairing of all stakes across all subnets. + """ + if netuids is None: + all_netuids = self.get_subnets(block=block) + else: + all_netuids = netuids + results = [ + self.query_runtime_api( + "StakeInfoRuntimeApi", + "get_stake_info_for_hotkey_coldkey_netuid", + params=[hotkey_ss58, coldkey_ss58, netuid], + block=block, + ) + for netuid in all_netuids + ] + return { + netuid: StakeInfo.from_dict(result) + for (netuid, result) in zip(all_netuids, results) + } + + def get_stake_for_coldkey( + self, coldkey_ss58: str, block: Optional[int] = None + ) -> list["StakeInfo"]: + """ + Retrieves the stake information for a given coldkey. + + Args: + coldkey_ss58 (str): The SS58 address of the coldkey. + block (Optional[int]): The block number at which to query the stake information. + + Returns: + Optional[list[StakeInfo]]: A list of StakeInfo objects, or ``None`` if no stake information is found. + """ + result = self.query_runtime_api( + runtime_api="StakeInfoRuntimeApi", + method="get_stake_info_for_coldkey", + params=[coldkey_ss58], # type: ignore + block=block, + ) + + if result is None: + return [] + stakes = StakeInfo.list_from_dicts(result) # type: ignore + return [stake for stake in stakes if stake.stake > 0] + + get_stake_info_for_coldkey = get_stake_for_coldkey + + def get_stake_for_hotkey( + self, hotkey_ss58: str, netuid: int, block: Optional[int] = None + ) -> Balance: + """ + Retrieves the stake information for a given hotkey. + + Args: + hotkey_ss58: The SS58 address of the hotkey. + netuid: The subnet ID to query for. + block: The block number at which to query the stake information. Do not specify if also specifying + block_hash or reuse_block + """ + hotkey_alpha_query = self.query_subtensor( + 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 + + get_hotkey_stake = get_stake_for_hotkey + + def get_stake_operations_fee( + self, + amount: Balance, + netuid: int, + block: Optional[int] = None, + ): + """Returns fee for any stake operation in specified subnet. + + Args: + amount: Amount of stake to add in Alpha/TAO. + netuid: Netuid of subnet. + block: Block number at which to perform the calculation. + + Returns: + The calculated stake fee as a Balance object. + """ + block_hash = self.determine_block_hash(block=block) + result = self.substrate.query( + module="Swap", + storage_function="FeeRate", + params=[netuid], + block_hash=block_hash, + ) + return amount * (result.value / U16_MAX) + + def get_stake_weight(self, netuid: int, block: Optional[int] = None) -> list[float]: + """ + Retrieves the stake weight for all hotkeys in a given subnet. + + Arguments: + netuid: Netuid of subnet. + block: Block number at which to perform the calculation. + + Returns: + A list of stake weights for all hotkeys in the specified subnet. + """ + block_hash = self.determine_block_hash(block=block) + result = self.substrate.query( + module="SubtensorModule", + storage_function="StakeWeight", + params=[netuid], + block_hash=block_hash, + ) + return [u16_normalized_float(w) for w in result] + + def get_subnet_burn_cost(self, block: Optional[int] = None) -> Optional[Balance]: + """ + Retrieves the burn cost for registering a new subnet within the Bittensor network. This cost represents the + amount of Tao that needs to be locked or burned to establish a new subnet. + + Arguments: + block (Optional[int]): The blockchain block number for the query. + + Returns: + int: The burn cost for subnet registration. + + The subnet burn cost is an important economic parameter, reflecting the network's mechanisms for controlling + the proliferation of subnets and ensuring their commitment to the network's long-term viability. + """ + lock_cost = self.query_runtime_api( + runtime_api="SubnetRegistrationRuntimeApi", + method="get_network_registration_cost", + params=[], + block=block, + ) + + if lock_cost is not None: + return Balance.from_rao(lock_cost) + else: + return lock_cost + + def get_subnet_hyperparameters( + self, netuid: int, block: Optional[int] = None + ) -> Optional[Union[list, "SubnetHyperparameters"]]: + """ + Retrieves the hyperparameters for a specific subnet within the Bittensor network. These hyperparameters define + the operational settings and rules governing the subnet's behavior. + + Arguments: + netuid (int): The network UID of the subnet to query. + block (Optional[int]): The blockchain block number for the query. + + Returns: + The subnet's hyperparameters, or `None` if not available. + + Understanding the hyperparameters is crucial for comprehending how subnets are configured and managed, and how + they interact with the network's consensus and incentive mechanisms. + """ + result = self.query_runtime_api( + runtime_api="SubnetInfoRuntimeApi", + method="get_subnet_hyperparams_v2", + params=[netuid], + block=block, + ) + + if not result: + return None + + return SubnetHyperparameters.from_dict(result) + + def get_subnet_reveal_period_epochs( + self, netuid: int, block: Optional[int] = None + ) -> int: + """Retrieve the SubnetRevealPeriodEpochs hyperparameter.""" + return cast( + int, + self.get_hyperparameter( + param_name="RevealPeriodEpochs", block=block, netuid=netuid + ), + ) + + def get_subnets(self, block: Optional[int] = None) -> list[int]: + """ + Retrieves the list of all subnet unique identifiers (netuids) currently present in the Bittensor network. + + Arguments: + block (Optional[int]): The blockchain block number for the query. + + Returns: + A list of subnet netuids. + + This function provides a comprehensive view of the subnets within the Bittensor network, + offering insights into its diversity and scale. + """ + result = self.substrate.query_map( + module="SubtensorModule", + storage_function="NetworksAdded", + block_hash=self.determine_block_hash(block), + ) + subnets = [] + if result.records: + for netuid, exists in result: + if exists: + subnets.append(netuid) + return subnets + + 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. + + Arguments: + block (Optional[int]): The blockchain block number for the query. + + Returns: + Optional[str]: The total number of subnets in the network. + + Understanding the total number of subnets is essential for assessing the network's growth and the extent of its + decentralized infrastructure. + """ + result = self.substrate.query( + module="SubtensorModule", + storage_function="TotalNetworks", + params=[], + block_hash=self.determine_block_hash(block), + ) + return getattr(result, "value", None) + + def get_transfer_fee( + self, + wallet: "Wallet", + dest: str, + value: Optional[Balance], + keep_alive: bool = True, + ) -> Balance: + """ + Calculates the transaction fee for transferring tokens from a wallet to a specified destination address. This + function simulates the transfer to estimate the associated cost, taking into account the current network + conditions and transaction complexity. + + Arguments: + wallet (bittensor_wallet.Wallet): The wallet from which the transfer is initiated. + dest (str): The ``SS58`` address of the destination account. + value (Union[bittensor.utils.balance.Balance, float, int]): The amount of tokens to be transferred, + specified as a Balance object, or in Tao (float) or Rao (int) units. + keep_alive: Whether the transfer fee should be calculated based on keeping the wallet alive (existential + deposit) or not. + + Returns: + bittensor.utils.balance.Balance: The estimated transaction fee for the transfer, represented as a Balance + object. + + Estimating the transfer fee is essential for planning and executing token transactions, ensuring that the wallet + has sufficient funds to cover both the transfer amount and the associated costs. This function provides a + crucial tool for managing financial operations within the Bittensor network. + """ + if value is not None: + value = check_and_convert_to_balance(value) + call_params: dict[str, Union[int, str, bool]] + call_function, call_params = get_transfer_fn_params(value, dest, keep_alive) + + call = self.substrate.compose_call( + call_module="Balances", + call_function=call_function, + call_params=call_params, + ) + + try: + payment_info = self.substrate.get_payment_info( + call=call, keypair=wallet.coldkeypub + ) + except Exception as e: + logging.error(f":cross_mark: [red]Failed to get payment info: [/red]{e}") + payment_info = {"partial_fee": int(2e7)} # assume 0.02 Tao + + return Balance.from_rao(payment_info["partial_fee"]) + + def get_vote_data( + self, proposal_hash: str, block: Optional[int] = None + ) -> Optional["ProposalVoteData"]: + """ + Retrieves the voting data for a specific proposal on the Bittensor blockchain. This data includes information + about how senate members have voted on the proposal. + + Arguments: + proposal_hash (str): The hash of the proposal for which voting data is requested. + block (Optional[int]): The blockchain block number for the query. + + Returns: + An object containing the proposal's voting data, or `None` if not found. + + 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: 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: + return None + + return ProposalVoteData.from_dict(vote_data) + + def get_uid_for_hotkey_on_subnet( + self, hotkey_ss58: str, netuid: int, block: Optional[int] = None + ) -> Optional[int]: + """ + Retrieves the unique identifier (UID) for a neuron's hotkey on a specific subnet. + + Arguments: + hotkey_ss58 (str): The ``SS58`` address of the neuron's hotkey. + netuid (int): The unique identifier of the subnet. + block (Optional[int]): The blockchain block number for the query. + + Returns: + Optional[int]: The UID of the neuron if it is registered on the subnet, ``None`` otherwise. + + The UID is a critical identifier within the network, linking the neuron's hotkey to its operational and + governance activities on a particular subnet. + """ + result = self.substrate.query( + module="SubtensorModule", + storage_function="Uids", + params=[netuid, hotkey_ss58], + block_hash=self.determine_block_hash(block), + ) + return getattr(result, "value", result) + + def filter_netuids_by_registered_hotkeys( + self, + all_netuids: Iterable[int], + filter_for_netuids: Iterable[int], + all_hotkeys: Iterable["Wallet"], + block: Optional[int], + ) -> list[int]: + """ + Filters a given list of all netuids for certain specified netuids and hotkeys + + Arguments: + all_netuids (Iterable[int]): A list of netuids to filter. + filter_for_netuids (Iterable[int]): A subset of all_netuids to filter from the main list. + all_hotkeys (Iterable[Wallet]): Hotkeys to filter from the main list. + block (Optional[int]): The blockchain block number for the query. + + Returns: + The filtered list of netuids. + """ + self._get_block_hash(block) # just used to cache the block hash + netuids_with_registered_hotkeys = [ + item + for sublist in [ + self.get_netuids_for_hotkey( + wallet.hotkey.ss58_address, + block=block, + ) + for wallet in all_hotkeys + ] + for item in sublist + ] + + if not filter_for_netuids: + all_netuids = netuids_with_registered_hotkeys + + else: + filtered_netuids = [ + netuid for netuid in all_netuids if netuid in filter_for_netuids + ] + + registered_hotkeys_filtered = [ + netuid + for netuid in netuids_with_registered_hotkeys + if netuid in filter_for_netuids + ] + + # Combine both filtered lists + all_netuids = filtered_netuids + registered_hotkeys_filtered + + return list(set(all_netuids)) + + def immunity_period( + self, netuid: int, block: Optional[int] = None + ) -> Optional[int]: + """ + Retrieves the 'ImmunityPeriod' hyperparameter for a specific subnet. This parameter defines the duration during + which new neurons are protected from certain network penalties or restrictions. + + Args: + netuid (int): The unique identifier of the subnet. + block (Optional[int]): The blockchain block number for the query. + + Returns: + Optional[int]: The value of the 'ImmunityPeriod' hyperparameter if the subnet exists, ``None`` otherwise. + + The 'ImmunityPeriod' is a critical aspect of the network's governance system, ensuring that new participants + have a grace period to establish themselves and contribute to the network without facing immediate + punitive actions. + """ + call = self.get_hyperparameter( + param_name="ImmunityPeriod", netuid=netuid, block=block + ) + return None if call is None else int(call) + + def is_fast_blocks(self): + """Returns True if the node is running with fast blocks. False if not.""" + return self.query_constant("SubtensorModule", "DurationOfStartCall").value == 10 + + def is_hotkey_delegate(self, hotkey_ss58: str, block: Optional[int] = None) -> bool: + """ + Determines whether a given hotkey (public key) is a delegate on the Bittensor network. This function checks if + the neuron associated with the hotkey is part of the network's delegation system. + + Arguments: + hotkey_ss58 (str): The SS58 address of the neuron's hotkey. + block (Optional[int]): The blockchain block number for the query. + + Returns: + `True` if the hotkey is a delegate, `False` otherwise. + + Being a delegate is a significant status within the Bittensor network, indicating a neuron's involvement in + consensus and governance processes. + """ + delegates = self.get_delegates(block) + return hotkey_ss58 in [info.hotkey_ss58 for info in delegates] + + def is_hotkey_registered( + self, + hotkey_ss58: str, + netuid: Optional[int] = None, + block: Optional[int] = None, + ) -> bool: + """ + Determines whether a given hotkey (public key) is registered in the Bittensor network, either globally across + any subnet or specifically on a specified subnet. This function checks the registration status of a neuron + identified by its hotkey, which is crucial for validating its participation and activities within the + network. + + Args: + hotkey_ss58: The SS58 address of the neuron's hotkey. + netuid: The unique identifier of the subnet to check the registration. If `None`, the + registration is checked across all subnets. + block: The blockchain block number at which to perform the query. + + Returns: + bool: `True` if the hotkey is registered in the specified context (either any subnet or a specific subnet), + `False` otherwise. + + This function is important for verifying the active status of neurons in the Bittensor network. It aids in + understanding whether a neuron is eligible to participate in network processes such as consensus, + validation, and incentive distribution based on its registration status. + """ + if netuid is None: + return self.is_hotkey_registered_any(hotkey_ss58, block) + else: + return self.is_hotkey_registered_on_subnet(hotkey_ss58, netuid, block) + + def is_hotkey_registered_any( + self, + hotkey_ss58: str, + block: Optional[int] = None, + ) -> bool: + """ + Checks if a neuron's hotkey is registered on any subnet within the Bittensor network. + + Arguments: + hotkey_ss58 (str): The ``SS58`` address of the neuron's hotkey. + block (Optional[int]): The blockchain block number for the query. + + Returns: + bool: ``True`` if the hotkey is registered on any subnet, False otherwise. + + This function is essential for determining the network-wide presence and participation of a neuron. + """ + hotkeys = self.get_netuids_for_hotkey(hotkey_ss58, block) + return len(hotkeys) > 0 + + def is_hotkey_registered_on_subnet( + self, hotkey_ss58: str, netuid: int, block: Optional[int] = None + ) -> bool: + """Checks if the hotkey is registered on a given netuid.""" + return ( + self.get_uid_for_hotkey_on_subnet(hotkey_ss58, netuid, block=block) + is not None + ) + + def is_subnet_active(self, netuid: int, block: Optional[int] = None) -> bool: + """Verify if subnet with provided netuid is active. + + Args: + netuid (int): The unique identifier of the subnet. + block (Optional[int]): The blockchain block number for the query. + + Returns: + True if subnet is active, False otherwise. + + This means whether the `start_call` was initiated or not. + """ + query = self.query_subtensor( + name="FirstEmissionBlockNumber", + block=block, + params=[netuid], + ) + return True if query and query.value > 0 else False + + def last_drand_round(self) -> Optional[int]: + """ + Retrieves the last drand round emitted in bittensor. This corresponds when committed weights will be revealed. + + Returns: + int: The latest Drand round emitted in bittensor. + """ + result = self.substrate.query( + module="Drand", storage_function="LastStoredRound" + ) + return getattr(result, "value", None) + + def max_weight_limit( + self, netuid: int, block: Optional[int] = None + ) -> Optional[float]: + """ + Returns network MaxWeightsLimit hyperparameter. + + Args: + netuid (int): The unique identifier of the subnetwork. + block (Optional[int]): The blockchain block number for the query. + + Returns: + Optional[float]: The value of the MaxWeightsLimit hyperparameter, or ``None`` if the subnetwork does not + exist or the parameter is not found. + """ + call = self.get_hyperparameter( + param_name="MaxWeightsLimit", netuid=netuid, block=block + ) + return None if call is None else u16_normalized_float(int(call)) + + def metagraph( + self, netuid: int, lite: bool = True, block: Optional[int] = None + ) -> "Metagraph": + metagraph = Metagraph( + network=self.chain_endpoint, + netuid=netuid, + lite=lite, + sync=False, + subtensor=self, + ) + metagraph.sync(block=block, lite=lite, subtensor=self) + + return metagraph + + def min_allowed_weights( + self, netuid: int, block: Optional[int] = None + ) -> Optional[int]: + """ + Returns network MinAllowedWeights hyperparameter. + + Args: + netuid (int): The unique identifier of the subnetwork. + block (Optional[int]): The blockchain block number for the query. + + Returns: + Optional[int]: The value of the MinAllowedWeights hyperparameter, or ``None`` if the subnetwork does not + exist or the parameter is not found. + """ + call = self.get_hyperparameter( + param_name="MinAllowedWeights", netuid=netuid, block=block + ) + return None if call is None else int(call) + + def neuron_for_uid( + self, uid: int, netuid: int, block: Optional[int] = None + ) -> "NeuronInfo": + """ + Retrieves detailed information about a specific neuron identified by its unique identifier (UID) within a + specified subnet (netuid) of the Bittensor network. This function provides a comprehensive view of a + neuron's attributes, including its stake, rank, and operational status. + + Arguments: + uid (int): The unique identifier of the neuron. + netuid (int): The unique identifier of the subnet. + block (Optional[int]): The blockchain block number for the query. + + Returns: + Detailed information about the neuron if found, a null neuron otherwise + + This function is crucial for analyzing individual neurons' contributions and status within a specific subnet, + offering insights into their roles in the network's consensus and validation mechanisms. + """ + if uid is None: + return NeuronInfo.get_null_neuron() + + result = self.query_runtime_api( + runtime_api="NeuronInfoRuntimeApi", + method="get_neuron", + params=[netuid, uid], + block=block, + ) + + if not result: + return NeuronInfo.get_null_neuron() + + return NeuronInfo.from_dict(result) + + def neurons(self, netuid: int, block: Optional[int] = None) -> list["NeuronInfo"]: + """ + Retrieves a list of all neurons within a specified subnet of the Bittensor network. + This function provides a snapshot of the subnet's neuron population, including each neuron's attributes and + network interactions. + + Arguments: + netuid (int): The unique identifier of the subnet. + block (Optional[int]): The blockchain block number for the query. + + Returns: + A list of NeuronInfo objects detailing each neuron's characteristics in the subnet. + + Understanding the distribution and status of neurons within a subnet is key to comprehending the network's + decentralized structure and the dynamics of its consensus and governance processes. + """ + result = self.query_runtime_api( + runtime_api="NeuronInfoRuntimeApi", + method="get_neurons", + params=[netuid], + block=block, + ) + + if not result: + return [] + + return NeuronInfo.list_from_dicts(result) + + def neurons_lite( + self, netuid: int, block: Optional[int] = None + ) -> list["NeuronInfoLite"]: + """ + Retrieves a list of neurons in a 'lite' format from a specific subnet of the Bittensor network. + This function provides a streamlined view of the neurons, focusing on key attributes such as stake and network + participation. + + Arguments: + netuid (int): The unique identifier of the subnet. + block (Optional[int]): The blockchain block number for the query. + + Returns: + A list of simplified neuron information for the subnet. + + This function offers a quick overview of the neuron population within a subnet, facilitating efficient analysis + of the network's decentralized structure and neuron dynamics. + """ + result = self.query_runtime_api( + runtime_api="NeuronInfoRuntimeApi", + method="get_neurons_lite", + params=[netuid], + block=block, + ) + + if not result: + return [] + + return NeuronInfoLite.list_from_dicts(result) + + def query_identity( + self, coldkey_ss58: str, block: Optional[int] = None + ) -> Optional[ChainIdentity]: + """ + Queries the identity of a neuron on the Bittensor blockchain using the given key. This function retrieves + detailed identity information about a specific neuron, which is a crucial aspect of the network's + decentralized identity and governance system. + + Arguments: + coldkey_ss58 (str): The coldkey used to query the neuron's identity (technically the neuron's coldkey SS58 + address). + block (Optional[int]): The blockchain block number for the query. + + Returns: + An object containing the identity information of the neuron if found, ``None`` otherwise. + + The identity information can include various attributes such as the neuron's stake, rank, and other + network-specific details, providing insights into the neuron's role and status within the Bittensor network. + + Note: + See the `Bittensor CLI documentation `_ for supported identity + parameters. + """ + identity_info = cast( + dict, + 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: + return ChainIdentity.from_dict( + decode_hex_identity_dict(identity_info), + ) + except TypeError: + return None + + def recycle(self, netuid: int, block: Optional[int] = None) -> Optional[Balance]: + """ + Retrieves the 'Burn' hyperparameter for a specified subnet. The 'Burn' parameter represents the amount of Tao + that is effectively recycled within the Bittensor network. + + Args: + netuid (int): The unique identifier of the subnet. + block (Optional[int]): The blockchain block number for the query. + + Returns: + Optional[Balance]: The value of the 'Burn' hyperparameter if the subnet exists, None otherwise. + + Understanding the 'Burn' rate is essential for analyzing the network registration usage, particularly how it is + correlated with user activity and the overall cost of participation in a given subnet. + """ + call = self.get_hyperparameter(param_name="Burn", netuid=netuid, block=block) + return None if call is None else Balance.from_rao(int(call)) + + def set_reveal_commitment( + self, + wallet, + netuid: int, + data: str, + blocks_until_reveal: int = 360, + block_time: Union[int, float] = 12, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + ) -> tuple[bool, int]: + """ + Commits arbitrary data to the Bittensor network by publishing metadata. + + Parameters: + wallet: The wallet associated with the neuron committing the data. + netuid: The unique identifier of the subnetwork. + data: The data to be committed to the network. + blocks_until_reveal: The number of blocks from now after which the data will be revealed. Then number of + blocks in one epoch. + block_time: The number of seconds between each block. + 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. + + Returns: + bool: `True` if the commitment was successful, `False` otherwise. + + Note: A commitment can be set once per subnet epoch and is reset at the next epoch in the chain automatically. + """ + + encrypted, reveal_round = get_encrypted_commitment( + data, blocks_until_reveal, block_time + ) + + # increase reveal_round in return + 1 because we want to fetch data from the chain after that round was revealed + # and stored. + data_ = {"encrypted": encrypted, "reveal_round": reveal_round} + return publish_metadata( + subtensor=self, + wallet=wallet, + netuid=netuid, + data_type="TimelockEncrypted", + data=data_, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ), reveal_round + + def subnet(self, netuid: int, block: Optional[int] = None) -> Optional[DynamicInfo]: + """ + Retrieves the subnet information for a single subnet in the network. + + Args: + netuid (int): The unique identifier of the subnet. + block (Optional[int]): The block number to query the subnet information from. + + Returns: + Optional[DynamicInfo]: A DynamicInfo object, containing detailed information about a subnet. + + """ + 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, + ) + + if isinstance(decoded := query.decode(), dict): + try: + price = self.get_subnet_price(netuid=netuid, block=block) + except (SubstrateRequestException, ValueError): + price = None + return DynamicInfo.from_dict({**decoded, "price": price}) + return None + + def subnet_exists(self, netuid: int, block: Optional[int] = None) -> bool: + """ + Checks if a subnet with the specified unique identifier (netuid) exists within the Bittensor network. + + Arguments: + netuid (int): The unique identifier of the subnet. + block (Optional[int]): The blockchain block number for the query. + + Returns: + `True` if the subnet exists, `False` otherwise. + + This function is critical for verifying the presence of specific subnets in the network, + enabling a deeper understanding of the network's structure and composition. + """ + result = self.substrate.query( + module="SubtensorModule", + storage_function="NetworksAdded", + params=[netuid], + block_hash=self.determine_block_hash(block), + ) + return getattr(result, "value", False) + + def subnetwork_n(self, netuid: int, block: Optional[int] = None) -> Optional[int]: + """ + Returns network SubnetworkN hyperparameter. + + Args: + netuid (int): The unique identifier of the subnetwork. + block (Optional[int]): The blockchain block number for the query. + + Returns: + Optional[int]: The value of the SubnetworkN hyperparameter, or ``None`` if the subnetwork does not exist or + the parameter is not found. + """ + call = self.get_hyperparameter( + param_name="SubnetworkN", netuid=netuid, block=block + ) + return None if call is None else int(call) + + def tempo(self, netuid: int, block: Optional[int] = None) -> Optional[int]: + """ + Returns network Tempo hyperparameter. + + Args: + netuid (int): The unique identifier of the subnetwork. + block (Optional[int]): The blockchain block number for the query. + + Returns: + Optional[int]: The value of the Tempo hyperparameter, or ``None`` if the subnetwork does not exist or the + parameter is not found. + """ + call = self.get_hyperparameter(param_name="Tempo", netuid=netuid, block=block) + return None if call is None else int(call) + + def tx_rate_limit(self, block: Optional[int] = None) -> Optional[int]: + """ + Retrieves the transaction rate limit for the Bittensor network as of a specific blockchain block. + This rate limit sets the maximum number of transactions that can be processed within a given time frame. + + Args: + block (Optional[int]): The blockchain block number for the query. + + Returns: + Optional[int]: The transaction rate limit of the network, None if not available. + + The transaction rate limit is an essential parameter for ensuring the stability and scalability of the Bittensor + network. It helps in managing network load and preventing congestion, thereby maintaining efficient and + timely transaction processing. + """ + result = self.query_subtensor("TxRateLimit", block=block) + return getattr(result, "value", None) + + def wait_for_block(self, block: Optional[int] = None): + """ + Waits until a specific block is reached on the chain. If no block is specified, + waits for the next block. + + Args: + block (Optional[int]): The block number to wait for. If None, waits for the next block. + + Returns: + bool: True if the target block was reached, False if timeout occurred. + + Example: + import bittensor as bt + subtensor = bt.Subtensor() + + subtensor.wait_for_block() # Waits for the next block + subtensor.wait_for_block(block=1234) # Waits for a specific block + """ + + def handler(block_data: dict): + logging.debug( + f"reached block {block_data['header']['number']}. Waiting for block {target_block}" + ) + if block_data["header"]["number"] >= target_block: + return True + return None + + current_block = self.substrate.get_block() + current_block_hash = current_block.get("header", {}).get("hash") + if block is not None: + target_block = block + else: + target_block = current_block["header"]["number"] + 1 + + self.substrate.get_block_handler( + current_block_hash, header_only=True, subscription_handler=handler + ) + return True + + def weights( + self, netuid: int, block: Optional[int] = None + ) -> list[tuple[int, list[tuple[int, int]]]]: + """ + Retrieves the weight distribution set by neurons within a specific subnet of the Bittensor network. + This function maps each neuron's UID to the weights it assigns to other neurons, reflecting the network's trust + and value assignment mechanisms. + + Arguments: + netuid (int): The network UID of the subnet to query. + block (Optional[int]): Block number for synchronization, or ``None`` for the latest block. + + Returns: + A list of tuples mapping each neuron's UID to its assigned weights. + + The weight distribution is a key factor in the network's consensus algorithm and the ranking of neurons, + influencing their influence and reward allocation within the subnet. + """ + w_map_encoded = self.substrate.query_map( + module="SubtensorModule", + storage_function="Weights", + params=[netuid], + block_hash=self.determine_block_hash(block), + ) + w_map = [(uid, w.value or []) for uid, w in w_map_encoded] + + return w_map + + def weights_rate_limit( + self, netuid: int, block: Optional[int] = None + ) -> Optional[int]: + """ + Returns network WeightsSetRateLimit hyperparameter. + + Arguments: + netuid (int): The unique identifier of the subnetwork. + block (Optional[int]): The blockchain block number for the query. + + Returns: + Optional[int]: The value of the WeightsSetRateLimit hyperparameter, or ``None`` if the subnetwork does not + exist or the parameter is not found. + """ + call = self.get_hyperparameter( + param_name="WeightsSetRateLimit", netuid=netuid, block=block + ) + return None if call is None else int(call) + + def get_timestamp(self, block: Optional[int] = None) -> datetime: + """ + Retrieves the datetime timestamp for a given block + + Arguments: + block: The blockchain block number for the query. + + 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) + + def get_subnet_owner_hotkey( + self, netuid: int, block: Optional[int] = None + ) -> Optional[str]: + """ + Retrieves the hotkey of the subnet owner for a given network UID. + + This function queries the subtensor network to fetch the hotkey of the owner of a subnet specified by its + netuid. If no data is found or the query fails, the function returns None. + + Arguments: + netuid: The network UID of the subnet to fetch the owner's hotkey for. + block: The specific block number to query the data from. + + Returns: + The hotkey of the subnet owner if available; None otherwise. + """ + return self.query_subtensor( + name="SubnetOwnerHotkey", params=[netuid], block=block + ) + + def get_subnet_validator_permits( + self, netuid: int, block: Optional[int] = None + ) -> Optional[list[bool]]: + """ + Retrieves the list of validator permits for a given subnet as boolean values. + + Arguments: + netuid: The unique identifier of the subnetwork. + block: The blockchain block number for the query. + + Returns: + A list of boolean values representing validator permits, or None if not available. + """ + query = self.query_subtensor( + name="ValidatorPermit", + params=[netuid], + block=block, + ) + return query.value if query is not None and hasattr(query, "value") else query + + # Extrinsics helper ================================================================================================ + + def sign_and_send_extrinsic( + self, + call: "GenericCall", + wallet: "Wallet", + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, + sign_with: str = "coldkey", + use_nonce: bool = False, + period: Optional[int] = None, + nonce_key: str = "hotkey", + raise_error: bool = False, + ) -> tuple[bool, str]: + """ + Helper method to sign and submit an extrinsic call to chain. + + Arguments: + call (scalecodec.types.GenericCall): a prepared Call object + wallet (bittensor_wallet.Wallet): the wallet whose coldkey will be used to sign the extrinsic + wait_for_inclusion (bool): whether to wait until the extrinsic call is included on the chain + wait_for_finalization (bool): whether to wait until the extrinsic call is finalized on the chain + sign_with (str): the wallet's keypair to use for the signing. Options are "coldkey", "hotkey", "coldkeypub" + use_nonce (bool): unique identifier for the transaction related with hot/coldkey. + period (Optional[int]): 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. + nonce_key: the type on nonce to use. Options are "hotkey" or "coldkey". + raise_error: raises the relevant exception rather than returning `False` if unsuccessful. + + Returns: + (success, error message) + + Raises: + SubstrateRequestException: Substrate request exception. + """ + possible_keys = ("coldkey", "hotkey", "coldkeypub") + if sign_with not in possible_keys: + raise AttributeError( + f"'sign_with' must be either 'coldkey', 'hotkey' or 'coldkeypub', not '{sign_with}'" + ) + + signing_keypair = getattr(wallet, sign_with) + extrinsic_data = {"call": call, "keypair": signing_keypair} + if use_nonce: + if nonce_key not in possible_keys: + raise AttributeError( + f"'nonce_key' must be either 'coldkey', 'hotkey' or 'coldkeypub', not '{nonce_key}'" + ) + next_nonce = self.substrate.get_account_next_index( + getattr(wallet, nonce_key).ss58_address + ) + extrinsic_data["nonce"] = next_nonce + if period is not None: + extrinsic_data["era"] = {"period": period} + + extrinsic = self.substrate.create_signed_extrinsic(**extrinsic_data) + try: + response = self.substrate.submit_extrinsic( + extrinsic, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + # We only wait here if we expect finalization. + if not wait_for_finalization and not wait_for_inclusion: + message = "Not waiting for finalization or inclusion." + logging.debug(f"{message}. Extrinsic: {extrinsic}") + return True, message + + if response.is_success: + return True, "" + + if raise_error: + raise ChainError.from_error(response.error_message) + + return False, format_error_message(response.error_message) + + except SubstrateRequestException as e: + if raise_error: + raise + + return False, format_error_message(e) + + # Extrinsics ======================================================================================================= + + def add_stake( + self, + wallet: "Wallet", + netuid: int, + hotkey_ss58: str, + amount: Balance, + safe_staking: bool = False, + allow_partial_stake: bool = False, + rate_tolerance: float = 0.005, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + ) -> bool: + """ + Adds a stake from the specified wallet to the neuron identified by the SS58 address of its hotkey in specified + subnet. Staking is a fundamental process in the Bittensor network that enables neurons to participate actively + and earn incentives. + + Parameters: + wallet: The wallet to be used for staking. + netuid: The unique identifier of the subnet to which the neuron belongs. + hotkey_ss58: The `ss58` address of the hotkey account to stake to default to the wallet's hotkey. + amount: The amount of TAO to stake. + safe_staking: If true, enables price safety checks to protect against fluctuating prices. The stake will + only execute if the price change doesn't exceed the rate tolerance. Default is ``False``. + allow_partial_stake: If true and safe_staking is enabled, allows partial staking when the full amount would + exceed the price tolerance. If false, the entire stake fails if it would exceed the tolerance. + Default is ``False``. + rate_tolerance: The maximum allowed price change ratio when staking. For example, 0.005 = 0.5% maximum price + increase. Only used when safe_staking is True. + 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 extrinsic to be included in a block. + wait_for_finalization: Whether to wait for finalization of the extrinsic. + + Returns: + bool: ``True`` if the staking is successful, ``False`` otherwise. + + This function enables neurons to increase their stake in the network, enhancing their influence and potential. + When safe_staking is enabled, it provides protection against price fluctuations during the time stake is + executed and the time it is actually processed by the chain. + """ + amount = check_and_convert_to_balance(amount) + return add_stake_extrinsic( + subtensor=self, + wallet=wallet, + hotkey_ss58=hotkey_ss58, + netuid=netuid, + amount=amount, + safe_staking=safe_staking, + allow_partial_stake=allow_partial_stake, + rate_tolerance=rate_tolerance, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + def add_liquidity( + self, + wallet: "Wallet", + netuid: int, + liquidity: Balance, + price_low: Balance, + price_high: Balance, + hotkey: Optional[str] = None, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + ) -> tuple[bool, str]: + """ + Adds liquidity to the specified price range. + + Parameters: + wallet: The wallet used to sign the extrinsic (must be unlocked). + netuid: The UID of the target subnet for which the call is being initiated. + liquidity: The amount of liquidity to be added. + price_low: The lower bound of the price tick range. In TAO. + price_high: The upper bound of the price tick range. In TAO. + hotkey: The hotkey with staked TAO in Alpha. If not passed then the wallet hotkey is used. + 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 extrinsic to be included in a block. + wait_for_finalization: Whether to wait for finalization of the extrinsic. + + Returns: + Tuple[bool, str]: + - True and a success message if the extrinsic is successfully submitted or processed. + - False and an error message if the submission fails or the wallet cannot be unlocked. + + Note: Adding is allowed even when user liquidity is enabled in specified subnet. Call `toggle_user_liquidity` + method to enable/disable user liquidity. + """ + return add_liquidity_extrinsic( + subtensor=self, + wallet=wallet, + netuid=netuid, + liquidity=liquidity, + price_low=price_low, + price_high=price_high, + hotkey=hotkey, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + def add_stake_multiple( + self, + wallet: "Wallet", + hotkey_ss58s: list[str], + netuids: list[int], + amounts: list[Balance], + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + ) -> bool: + """ + Adds stakes to multiple neurons identified by their hotkey SS58 addresses. + This bulk operation allows for efficient staking across different neurons from a single wallet. + + Parameters: + wallet: The wallet used for staking. + hotkey_ss58s: List of ``SS58`` addresses of hotkeys to stake to. + netuids: List of subnet UIDs. + amounts: List of corresponding TAO amounts to bet for each netuid and hotkey. + 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. + + Returns: + bool: ``True`` if the staking is successful for all specified neurons, ``False`` otherwise. + + This function is essential for managing stakes across multiple neurons, reflecting the dynamic and collaborative + nature of the Bittensor network. + """ + return add_stake_multiple_extrinsic( + subtensor=self, + wallet=wallet, + netuids=netuids, + hotkey_ss58s=hotkey_ss58s, + amounts=amounts, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + def burned_register( + self, + wallet: "Wallet", + netuid: int, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + ) -> bool: + """ + Registers a neuron on the Bittensor network by recycling TAO. This method of registration involves recycling + TAO tokens, allowing them to be re-mined by performing work on the network. + + Parameters: + wallet: The wallet associated with the neuron to be registered. + netuid: The unique identifier of the subnet. + 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. + + Returns: + bool: ``True`` if the registration is successful, False otherwise. + """ + + if netuid == 0: + return root_register_extrinsic( + subtensor=self, + wallet=wallet, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + return burned_register_extrinsic( + subtensor=self, + wallet=wallet, + netuid=netuid, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + def commit_weights( + self, + wallet: "Wallet", + netuid: int, + salt: list[int], + uids: Union[NDArray[np.int64], list], + weights: Union[NDArray[np.int64], list], + version_key: int = version_as_int, + max_retries: int = 5, + period: Optional[int] = 16, + raise_error: bool = True, + wait_for_inclusion: bool = False, + wait_for_finalization: bool = False, + ) -> tuple[bool, str]: + """ + Commits a hash of the neuron's weights to the Bittensor blockchain using the provided wallet. + This action serves as a commitment or snapshot of the neuron's current weight distribution. + + Parameters: + wallet: The wallet associated with the neuron committing the weights. + netuid: The unique identifier of the subnet. + salt: list of randomly generated integers as salt to generated weighted hash. + uids: NumPy array of neuron UIDs for which weights are being committed. + weights: NumPy array of weight values corresponding to each UID. + version_key: Version key for compatibility with the network. + max_retries: The number of maximum attempts to commit weights. + 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 extrinsic to be included in a block. + wait_for_finalization: Whether to wait for finalization of the extrinsic. + + Returns: + tuple[bool, str]: + `True` if the weight commitment is successful, False otherwise. + `msg` is a string value describing the success or potential error. + + This function allows neurons to create a tamper-proof record of their weight distribution at a specific point in + time, enhancing transparency and accountability within the Bittensor network. + """ + retries = 0 + success = False + message = "No attempt made. Perhaps it is too soon to commit weights!" + + logging.info( + f"Committing weights with params: " + f"netuid=[blue]{netuid}[/blue], uids=[blue]{uids}[/blue], weights=[blue]{weights}[/blue], " + f"version_key=[blue]{version_key}[/blue]" + ) + + # Generate the hash of the weights + commit_hash = generate_weight_hash( + address=wallet.hotkey.ss58_address, + netuid=netuid, + uids=list(uids), + values=list(weights), + salt=salt, + version_key=version_key, + ) + + while retries < max_retries and success is False: + try: + success, message = commit_weights_extrinsic( + subtensor=self, + wallet=wallet, + netuid=netuid, + commit_hash=commit_hash, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + if success: + break + except Exception as e: + logging.error(f"Error committing weights: {e}") + retries += 1 + + return success, message + + def modify_liquidity( + self, + wallet: "Wallet", + netuid: int, + position_id: int, + liquidity_delta: Balance, + hotkey: Optional[str] = None, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + ) -> tuple[bool, str]: + """Modifies liquidity in liquidity position by adding or removing liquidity from it. + + Parameters: + wallet: The wallet used to sign the extrinsic (must be unlocked). + netuid: The UID of the target subnet for which the call is being initiated. + position_id: The id of the position record in the pool. + liquidity_delta: The amount of liquidity to be added or removed (add if positive or remove if negative). + hotkey: The hotkey with staked TAO in Alpha. If not passed then the wallet hotkey is used. + 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 extrinsic to be included in a block. + wait_for_finalization: Whether to wait for finalization of the extrinsic. + + Returns: + Tuple[bool, str]: + - True and a success message if the extrinsic is successfully submitted or processed. + - False and an error message if the submission fails or the wallet cannot be unlocked. + + Example: + import bittensor as bt + + subtensor = bt.subtensor(network="local") + my_wallet = bt.Wallet() + + # if `liquidity_delta` is negative + my_liquidity_delta = Balance.from_tao(100) * -1 + + subtensor.modify_liquidity( + wallet=my_wallet, + netuid=123, + position_id=2, + liquidity_delta=my_liquidity_delta + ) + + # if `liquidity_delta` is positive + my_liquidity_delta = Balance.from_tao(120) + + subtensor.modify_liquidity( + wallet=my_wallet, + netuid=123, + position_id=2, + liquidity_delta=my_liquidity_delta + ) + + Note: Modifying is allowed even when user liquidity is enabled in specified subnet. Call `toggle_user_liquidity` + to enable/disable user liquidity. + """ + return modify_liquidity_extrinsic( + subtensor=self, + wallet=wallet, + netuid=netuid, + position_id=position_id, + liquidity_delta=liquidity_delta, + hotkey=hotkey, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + def move_stake( + self, + wallet: "Wallet", + origin_hotkey_ss58: str, + origin_netuid: int, + destination_hotkey_ss58: str, + destination_netuid: int, + amount: Optional[Balance] = None, + move_all_stake: bool = False, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + ) -> bool: + """ + Moves stake to a different hotkey and/or subnet. + + Parameters: + wallet: The wallet to move stake from. + origin_hotkey_ss58: The SS58 address of the source hotkey. + origin_netuid: The netuid of the source subnet. + destination_hotkey_ss58: The SS58 address of the destination hotkey. + destination_netuid: The netuid of the destination subnet. + amount: Amount of stake to move. + move_all_stake: If true, moves all stake from the source hotkey to the destination hotkey. + 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. + + Returns: + success: True if the stake movement was successful. + """ + amount = check_and_convert_to_balance(amount) + return move_stake_extrinsic( + subtensor=self, + wallet=wallet, + origin_hotkey_ss58=origin_hotkey_ss58, + origin_netuid=origin_netuid, + destination_hotkey_ss58=destination_hotkey_ss58, + destination_netuid=destination_netuid, + amount=amount, + move_all_stake=move_all_stake, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + 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, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + ) -> bool: + """ + 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. + + 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. + 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. + + Returns: + bool: ``True`` if the registration is successful, False otherwise. + + This function facilitates the entry of new neurons into the network, supporting the decentralized growth and + scalability of the Bittensor ecosystem. + """ + return 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, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + def register_subnet( + self, + wallet: "Wallet", + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + ) -> bool: + """ + Registers a new subnetwork on the Bittensor network. + + Parameters: + wallet: The wallet to be used for subnet registration. + 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 extrinsic to be included in a block. + wait_for_finalization: Whether to wait for finalization of the extrinsic. + + Returns: + bool: True if the subnet registration was successful, False otherwise. + """ + return register_subnet_extrinsic( + subtensor=self, + wallet=wallet, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + def remove_liquidity( + self, + wallet: "Wallet", + netuid: int, + position_id: int, + hotkey: Optional[str] = None, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + ) -> tuple[bool, str]: + """Remove liquidity and credit balances back to wallet's hotkey stake. + + Parameters: + wallet: The wallet used to sign the extrinsic (must be unlocked). + netuid: The UID of the target subnet for which the call is being initiated. + position_id: The id of the position record in the pool. + hotkey: The hotkey with staked TAO in Alpha. If not passed then the wallet hotkey is used. + 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 extrinsic to be included in a block. + wait_for_finalization: Whether to wait for finalization of the extrinsic. + + Returns: + Tuple[bool, str]: + - True and a success message if the extrinsic is successfully submitted or processed. + - False and an error message if the submission fails or the wallet cannot be unlocked. + + Note: + - Adding is allowed even when user liquidity is enabled in specified subnet. Call `toggle_user_liquidity` + extrinsic to enable/disable user liquidity. + - To get the `position_id` use `get_liquidity_list` method. + """ + return remove_liquidity_extrinsic( + subtensor=self, + wallet=wallet, + netuid=netuid, + position_id=position_id, + hotkey=hotkey, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + def reveal_weights( + self, + wallet: "Wallet", + netuid: int, + uids: Union[NDArray[np.int64], list], + weights: Union[NDArray[np.int64], list], + salt: Union[NDArray[np.int64], list], + max_retries: int = 5, + version_key: int = version_as_int, + period: Optional[int] = 16, + raise_error: bool = True, + wait_for_inclusion: bool = False, + wait_for_finalization: bool = False, + ) -> tuple[bool, str]: + """ + Reveals the weights for a specific subnet on the Bittensor blockchain using the provided wallet. + This action serves as a revelation of the neuron's previously committed weight distribution. + + Parameters: + wallet: The wallet associated with the subnet validator revealing the weights. + netuid: unique identifier of the subnet. + uids: NumPy array of subnet miner neuron UIDs for which weights are being revealed. + weights: NumPy array of weight values corresponding to each UID. + salt: NumPy array of salt values + max_retries: The number of maximum attempts to reveal weights. + version_key: Version key for compatibility with the network. + 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 extrinsic to be included in a block. + wait_for_finalization: Whether to wait for finalization of the extrinsic. + + Returns: + Tuple[bool, str]: + - True and a success message if the extrinsic is successfully submitted or processed. + - False and an error message if the submission fails or the wallet cannot be unlocked. + + This function allows subnet validators to reveal their previously committed weight vector. + + See also: , + """ + retries = 0 + success = False + message = "No attempt made. Perhaps it is too soon to reveal weights!" + + while retries < max_retries and success is False: + try: + success, message = reveal_weights_extrinsic( + subtensor=self, + wallet=wallet, + netuid=netuid, + uids=list(uids), + weights=list(weights), + salt=list(salt), + version_key=version_key, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + period=period, + raise_error=raise_error, + ) + if success: + break + except Exception as e: + logging.error(f"Error revealing weights: {e}") + retries += 1 + + return success, message + + def root_register( + self, + wallet: "Wallet", + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + ) -> bool: + """ + Register neuron by recycling some TAO. + + Parameters: + wallet (bittensor_wallet.Wallet): Bittensor wallet instance. + 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. + + Returns: + bool: ``True`` if the registration is successful, False otherwise. + """ + + return root_register_extrinsic( + subtensor=self, + wallet=wallet, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + def root_set_pending_childkey_cooldown( + self, + wallet: "Wallet", + cooldown: int, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + ) -> tuple[bool, str]: + """Sets the pending childkey cooldown. + + Parameters: + wallet: bittensor wallet instance. + cooldown: the number of blocks to setting pending childkey cooldown. + period (Optional[int]): 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 (bool): Waits for the transaction to be included in a block. + wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. + + Returns: + Tuple[bool, str]: + - True and a success message if the extrinsic is successfully submitted or processed. + - False and an error message if the submission fails or the wallet cannot be unlocked. + + Note: This operation can only be successfully performed if your wallet has root privileges. + """ + return root_set_pending_childkey_cooldown_extrinsic( + subtensor=self, + wallet=wallet, + cooldown=cooldown, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + def set_children( + self, + wallet: "Wallet", + hotkey: str, + netuid: int, + children: list[tuple[float, str]], + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + ) -> tuple[bool, str]: + """ + Allows a coldkey to set children-keys. + + Parameters: + wallet: bittensor wallet instance. + hotkey: The ``SS58`` address of the neuron's hotkey. + netuid: The netuid value. + children: A list of children with their proportions. + 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. + + Returns: + Tuple[bool, str]: + - True and a success message if the extrinsic is successfully submitted or processed. + - False and an error message if the submission fails or the wallet cannot be unlocked. + """ + return set_children_extrinsic( + subtensor=self, + wallet=wallet, + hotkey=hotkey, + netuid=netuid, + children=children, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + def set_delegate_take( + self, + wallet: "Wallet", + hotkey_ss58: str, + take: float, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + raise_error: bool = False, + period: Optional[int] = None, + ) -> tuple[bool, str]: + """ + Sets the delegate 'take' percentage for a neuron identified by its hotkey. + The 'take' represents the percentage of rewards that the delegate claims from its nominators' stakes. + + Parameters: + wallet: bittensor wallet instance. + hotkey_ss58: The ``SS58`` address of the neuron's hotkey. + take: Percentage reward for the delegate. + 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. + + Returns: + Tuple[bool, str]: + - True and a success message if the extrinsic is successfully submitted or processed. + - False and an error message if the submission fails or the wallet cannot be unlocked. + + Raises: + DelegateTakeTooHigh: Delegate take is too high. + DelegateTakeTooLow: Delegate take is too low. + DelegateTxRateLimitExceeded: A transactor exceeded the rate limit for delegate transaction. + HotKeyAccountNotExists: The hotkey does not exist. + NonAssociatedColdKey: Request to stake, unstake, or subscribe is made by a coldkey that is not associated + with the hotkey account. + bittensor_wallet.errors.PasswordError: Decryption failed or wrong password for decryption provided. + bittensor_wallet.errors.KeyFileError: Failed to decode keyfile data. + + The delegate take is a critical parameter in the network's incentive structure, influencing the distribution of + rewards among neurons and their nominators. + """ + # u16 representation of the take + take_u16 = int(take * 0xFFFF) + + current_take = self.get_delegate_take(hotkey_ss58) + current_take_u16 = int(current_take * 0xFFFF) + + if current_take_u16 == take_u16: + logging.info(":white_heavy_check_mark: [green]Already Set[/green]") + return True, "" + + logging.info(f"Updating {hotkey_ss58} take: current={current_take} new={take}") + + extrinsic_call = ( + increase_take_extrinsic + if current_take_u16 < take_u16 + else decrease_take_extrinsic + ) + + success, message = extrinsic_call( + subtensor=self, + wallet=wallet, + hotkey_ss58=hotkey_ss58, + take=take_u16, + period=period, + raise_error=raise_error, + wait_for_finalization=wait_for_finalization, + wait_for_inclusion=wait_for_inclusion, + ) + + if success: + logging.info(":white_heavy_check_mark: [green]Take Updated[/green]") + + return success, message + + def set_subnet_identity( + self, + wallet: "Wallet", + netuid: int, + subnet_identity: SubnetIdentity, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + ) -> tuple[bool, str]: + """ + Sets the identity of a subnet for a specific wallet and network. + + Parameters: + wallet: The wallet instance that will authorize the transaction. + netuid: The unique ID of the network on which the operation takes place. + subnet_identity: The identity data of the subnet including attributes like name, GitHub repository, contact, + URL, discord, description, and any additional metadata. + 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. + + Returns: + Tuple[bool, str]: + - True and a success message if the extrinsic is successfully submitted or processed. + - False and an error message if the submission fails or the wallet cannot be unlocked. + """ + return set_subnet_identity_extrinsic( + subtensor=self, + wallet=wallet, + netuid=netuid, + 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.logo_url, + discord=subnet_identity.discord, + description=subnet_identity.description, + additional=subnet_identity.additional, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + def set_weights( + self, + wallet: "Wallet", + netuid: int, + uids: Union[NDArray[np.int64], "torch.LongTensor", list], + weights: Union[NDArray[np.float32], "torch.FloatTensor", list], + block_time: float = 12.0, + commit_reveal_version: int = 4, + max_retries: int = 5, + version_key: int = version_as_int, + period: Optional[int] = 8, + raise_error: bool = True, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + ) -> tuple[bool, str]: + """ + Sets the interneuronal weights for the specified neuron. This process involves specifying the influence or trust + a neuron places on other neurons in the network, which is a fundamental aspect of Bittensor's decentralized + learning architecture. + + Parameters: + wallet: The wallet associated with the subnet validator setting the weights. + netuid: The unique identifier of the subnet. + uids: The list of subnet miner neuron UIDs that the weights are being set for. + weights: The corresponding weights to be set for each UID, representing the validator's evaluation of each + miner's performance. + block_time: The number of seconds for block duration. + commit_reveal_version: The version of the chain commit-reveal protocol to use. + max_retries: The number of maximum attempts to set weights. + version_key: Version key for compatibility with the network. + 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. + + Returns: + Tuple[bool, str]: + - True and a success message if the extrinsic is successfully submitted or processed. + - False and an error message if the submission fails or the wallet cannot be unlocked. + + This function is crucial in shaping the network's collective intelligence, where each neuron's learning and + contribution are influenced by the weights it sets towards others. + + Notes: + See + """ + + def _blocks_weight_limit() -> bool: + bslu = cast(int, self.blocks_since_last_update(netuid, cast(int, uid))) + wrl = cast(int, self.weights_rate_limit(netuid)) + return bslu > wrl + + retries = 0 + success = False + message = "No attempt made. Perhaps it is too soon to commit weights!" + if ( + uid := self.get_uid_for_hotkey_on_subnet(wallet.hotkey.ss58_address, netuid) + ) is None: + return ( + False, + f"Hotkey {wallet.hotkey.ss58_address} not registered in subnet {netuid}", + ) + + if self.commit_reveal_enabled(netuid=netuid): + # go with `commit reveal v3` extrinsic + + while retries < max_retries and success is False and _blocks_weight_limit(): + logging.info( + f"Committing weights for subnet [blue]{netuid}[/blue]. " + f"Attempt [blue]{retries + 1}[blue] of [green]{max_retries}[/green]." + ) + try: + success, message = commit_reveal_extrinsic( + subtensor=self, + wallet=wallet, + netuid=netuid, + uids=uids, + weights=weights, + block_time=block_time, + commit_reveal_version=commit_reveal_version, + version_key=version_key, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + except Exception as e: + logging.error(f"Error setting weights: {e}") + retries += 1 + + return success, message + else: + # go with classic `set_weights_extrinsic` + + while retries < max_retries and success is False and _blocks_weight_limit(): + try: + logging.info( + f"Setting weights for subnet [blue]{netuid}[/blue]. " + f"Attempt [blue]{retries + 1}[/blue] of [green]{max_retries}[/green]." + ) + success, message = set_weights_extrinsic( + subtensor=self, + wallet=wallet, + netuid=netuid, + uids=uids, + weights=weights, + version_key=version_key, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + except Exception as e: + logging.error(f"Error setting weights: {e}") + retries += 1 + + return success, message + + def serve_axon( + self, + netuid: int, + axon: "Axon", + certificate: Optional[Certificate] = None, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + ) -> bool: + """ + Registers an ``Axon`` serving endpoint on the Bittensor network for a specific neuron. + + This function is used to set up the Axon, a key component of a neuron that handles incoming queries and data + processing tasks. + + Parameters: + netuid: The unique identifier of the subnetwork. + axon: The Axon instance to be registered for serving. + certificate: Certificate to use for TLS. If ``None``, no TLS will be used. + 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. + + Returns: + bool: ``True`` if the Axon serve registration is successful, False otherwise. + + By registering an Axon, the neuron becomes an active part of the network's distributed computing infrastructure, + contributing to the collective intelligence of Bittensor. + """ + return serve_axon_extrinsic( + subtensor=self, + netuid=netuid, + axon=axon, + certificate=certificate, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + def set_commitment( + self, + wallet: "Wallet", + netuid: int, + data: str, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + ) -> bool: + """ + Commits arbitrary data to the Bittensor network by publishing metadata. + + This method allows neurons to publish arbitrary data to the blockchain, which can be used for various purposes + such as sharing model updates, configuration data, or other network-relevant information. + + + Parameters: + wallet (bittensor_wallet.Wallet): The wallet associated with the neuron committing the data. + netuid (int): The unique identifier of the subnetwork. + data (str): The data to be committed to the network. + 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. + + Returns: + bool: `True` if the commitment was successful, `False` otherwise. + + Example: + # Commit some data to subnet 1 + success = await subtensor.commit(wallet=my_wallet, netuid=1, data="Hello Bittensor!") + + # Commit with custom period + success = await subtensor.commit(wallet=my_wallet, netuid=1, data="Model update v2.0", period=100) + + Note: See + """ + return publish_metadata( + subtensor=self, + wallet=wallet, + netuid=netuid, + data_type=f"Raw{len(data)}", + data=data.encode(), + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + def start_call( + self, + wallet: "Wallet", + netuid: int, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, + ) -> tuple[bool, str]: + """ + Submits a start_call extrinsic to the blockchain, to trigger the start call process for a subnet (used to start + a new subnet's emission mechanism). + + Parameters: + wallet: The wallet used to sign the extrinsic (must be unlocked). + netuid: The UID of the target subnet for which the call is being initiated. + 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. + + Returns: + Tuple[bool, str]: + - True and a success message if the extrinsic is successfully submitted or processed. + - False and an error message if the submission fails or the wallet cannot be unlocked. + """ + return start_call_extrinsic( + subtensor=self, + wallet=wallet, + netuid=netuid, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + def swap_stake( + self, + wallet: "Wallet", + hotkey_ss58: str, + origin_netuid: int, + destination_netuid: int, + amount: Balance, + safe_swapping: bool = False, + allow_partial_stake: bool = False, + rate_tolerance: float = 0.005, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + ) -> bool: + """ + Moves stake between subnets while keeping the same coldkey-hotkey pair ownership. + Like subnet hopping - same owner, same hotkey, just changing which subnet the stake is in. + + Parameters: + wallet: The wallet to swap stake from. + hotkey_ss58: The SS58 address of the hotkey whose stake is being swapped. + origin_netuid: The netuid from which stake is removed. + destination_netuid: The netuid to which stake is added. + amount: The amount to swap. + safe_swapping: If true, enables price safety checks to protect against fluctuating prices. The swap + will only execute if the price ratio between subnets doesn't exceed the rate tolerance. + allow_partial_stake: If true and safe_staking is enabled, allows partial stake swaps when the full amount + would exceed the price tolerance. If false, the entire swap fails if it would exceed the tolerance. + rate_tolerance: The maximum allowed increase in the price ratio between subnets + (origin_price/destination_price). For example, 0.005 = 0.5% maximum increase. Only used when + safe_staking is True. + 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. + + Returns: + success: True if the extrinsic was successful. + + The price ratio for swap_stake in safe mode is calculated as: origin_subnet_price / destination_subnet_price + When safe_staking is enabled, the swap will only execute if: + - With allow_partial_stake=False: The entire swap amount can be executed without the price ratio + increasing more than rate_tolerance + - With allow_partial_stake=True: A partial amount will be swapped up to the point where the + price ratio would increase by rate_tolerance + """ + amount = check_and_convert_to_balance(amount) + return swap_stake_extrinsic( + subtensor=self, + wallet=wallet, + hotkey_ss58=hotkey_ss58, + origin_netuid=origin_netuid, + destination_netuid=destination_netuid, + amount=amount, + safe_swapping=safe_swapping, + allow_partial_stake=allow_partial_stake, + rate_tolerance=rate_tolerance, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + def toggle_user_liquidity( + self, + wallet: "Wallet", + netuid: int, + enable: bool, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + ) -> tuple[bool, str]: + """Allow to toggle user liquidity for specified subnet. + + Parameters: + wallet: The wallet used to sign the extrinsic (must be unlocked). + netuid: The UID of the target subnet for which the call is being initiated. + enable: Boolean indicating whether to enable user liquidity. + 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 extrinsic to be included in a block. + wait_for_finalization: Whether to wait for finalization of the extrinsic. + + Returns: + Tuple[bool, str]: + - True and a success message if the extrinsic is successfully submitted or processed. + - False and an error message if the submission fails or the wallet cannot be unlocked. + + Note: The call can be executed successfully by the subnet owner only. + """ + return toggle_user_liquidity_extrinsic( + subtensor=self, + wallet=wallet, + netuid=netuid, + enable=enable, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + def transfer( + self, + wallet: "Wallet", + destination: str, + amount: Optional[Balance], + transfer_all: bool = False, + keep_alive: bool = True, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, + ) -> bool: + """ + Transfer token of amount to destination. + + Parameters: + wallet: Source wallet for the transfer. + destination: Destination address for the transfer. + amount: Number of tokens to transfer. `None` is transferring all. + transfer_all: Flag to transfer all tokens. Default is `False`. + keep_alive: Flag to keep the connection alive. Default is `True`. + 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 extrinsic to be included in a block. + wait_for_finalization: Whether to wait for finalization of the extrinsic. + + Returns: + `True` if the transferring was successful, otherwise `False`. + """ + if amount is not None: + amount = check_and_convert_to_balance(amount) + return transfer_extrinsic( + subtensor=self, + wallet=wallet, + destination=destination, + amount=amount, + transfer_all=transfer_all, + keep_alive=keep_alive, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + def transfer_stake( + self, + wallet: "Wallet", + destination_coldkey_ss58: str, + hotkey_ss58: str, + origin_netuid: int, + destination_netuid: int, + amount: Balance, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + ) -> bool: + """ + Transfers stake from one subnet to another while changing the coldkey owner. + + Parameters: + wallet: The wallet to transfer stake from. + destination_coldkey_ss58: The destination coldkey SS58 address. + hotkey_ss58: The hotkey SS58 address associated with the stake. + origin_netuid: The source subnet UID. + destination_netuid: The destination subnet UID. + amount: Amount to transfer. + 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 extrinsic to be included in a block. + wait_for_finalization: Whether to wait for finalization of the extrinsic. + + Returns: + success: True if the transfer was successful. + """ + amount = check_and_convert_to_balance(amount) + return transfer_stake_extrinsic( + subtensor=self, + wallet=wallet, + destination_coldkey_ss58=destination_coldkey_ss58, + hotkey_ss58=hotkey_ss58, + origin_netuid=origin_netuid, + destination_netuid=destination_netuid, + amount=amount, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + def unstake( + self, + wallet: "Wallet", + netuid: int, + hotkey_ss58: str, + amount: Balance, + allow_partial_stake: bool = False, + rate_tolerance: float = 0.005, + safe_unstaking: bool = False, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + ) -> bool: + """ + Removes a specified amount of stake from a single hotkey account. This function is critical for adjusting + individual neuron stakes within the Bittensor network. + + Parameters: + wallet: The wallet associated with the neuron from which the stake is being removed. + netuid: The unique identifier of the subnet. + hotkey_ss58: The ``SS58`` address of the hotkey account to unstake from. + amount: The amount of alpha to unstake. If not specified, unstakes all. Alpha amount. + allow_partial_stake: If true and safe_staking is enabled, allows partial unstaking when + the full amount would exceed the price tolerance. If false, the entire unstake fails if it would + exceed the tolerance. + rate_tolerance: The maximum allowed price change ratio when unstaking. For example, + 0.005 = 0.5% maximum price decrease. Only used when safe_staking is True. + safe_unstaking: If true, enables price safety checks to protect against fluctuating prices. The unstake + will only execute if the price change doesn't exceed the rate tolerance. + 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 extrinsic to be included in a block. + wait_for_finalization: Whether to wait for finalization of the extrinsic. + + Returns: + bool: ``True`` if the unstaking process is successful, False otherwise. + + This function supports flexible stake management, allowing neurons to adjust their network participation and + potential reward accruals. When safe_staking is enabled, it provides protection against price fluctuations + during the time unstake is executed and the time it is actually processed by the chain. + """ + amount = check_and_convert_to_balance(amount) + return unstake_extrinsic( + subtensor=self, + wallet=wallet, + netuid=netuid, + hotkey_ss58=hotkey_ss58, + amount=amount, + allow_partial_stake=allow_partial_stake, + rate_tolerance=rate_tolerance, + safe_unstaking=safe_unstaking, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + def unstake_all( + self, + wallet: "Wallet", + hotkey: str, + netuid: int, + rate_tolerance: Optional[float] = 0.005, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + ) -> tuple[bool, str]: + """Unstakes all TAO/Alpha associated with a hotkey from the specified subnets on the Bittensor network. + + Parameters: + wallet: The wallet of the stake owner. + hotkey: The SS58 address of the hotkey to unstake from. + netuid: The unique identifier of the subnet. + rate_tolerance: The maximum allowed price change ratio when unstaking. For example, 0.005 = 0.5% maximum + price decrease. If not passed (None), then unstaking goes without price limit. Default is 0.005. + 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 extrinsic to be included in a block. + wait_for_finalization: Whether to wait for finalization of the extrinsic. + + Returns: + tuple[bool, str]: + A tuple containing: + - `True` and a success message if the unstake operation succeeded; + - `False` and an error message otherwise. + + Example: + # If you would like to unstake all stakes in all subnets safely: + import bittensor as bt + + subtensor = bt.Subtensor() + wallet = bt.Wallet("my_wallet") + netuid = 14 + hotkey = "5%SOME_HOTKEY%" + + wallet_stakes = subtensor.get_stake_info_for_coldkey(coldkey_ss58=wallet.coldkey.ss58_address) + + for stake in wallet_stakes: + result = subtensor.unstake_all( + wallet=wallet, + hotkey_ss58=stake.hotkey_ss58, + netuid=stake.netuid, + ) + print(result) + + # If you would like to unstake all stakes in all subnets unsafely, use `rate_tolerance=None`: + import bittensor as bt + + subtensor = bt.AsyncSubtensor() + wallet = bt.Wallet("my_wallet") + netuid = 14 + hotkey = "5%SOME_HOTKEY_WHERE_IS_YOUR_STAKE_NOW%" + + wallet_stakes = await subtensor.get_stake_info_for_coldkey(coldkey_ss58=wallet.coldkey.ss58_address) + + for stake in wallet_stakes: + result = await subtensor.unstake_all( + wallet=wallet, + hotkey_ss58=stake.hotkey_ss58, + netuid=stake.netuid, + rate_tolerance=None, + ) + print(result) + """ + if netuid != 0: + logging.debug( + f"Unstaking without Alpha price control from subnet [blue]#{netuid}[/blue]." + ) + return unstake_all_extrinsic( + subtensor=self, + wallet=wallet, + hotkey=hotkey, + netuid=netuid, + rate_tolerance=rate_tolerance, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + def unstake_multiple( + self, + wallet: "Wallet", + netuids: list[int], + hotkey_ss58s: list[str], + amounts: Optional[list[Balance]] = None, + unstake_all: bool = False, + period: Optional[int] = None, + raise_error: bool = False, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + ) -> bool: + """ + Performs batch unstaking from multiple hotkey accounts, allowing a neuron to reduce its staked amounts + efficiently. This function is useful for managing the distribution of stakes across multiple neurons. + + Parameters: + wallet: The wallet linked to the coldkey from which the stakes are being withdrawn. + netuids: Subnets unique IDs. + hotkey_ss58s: A list of hotkey `SS58` addresses to unstake from. + amounts: The amounts of TAO to unstake from each hotkey. If not provided, unstakes all. + unstake_all: If true, unstakes all tokens. Default is `False`. If `True` amounts are ignored. + 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 extrinsic to be included in a block. + wait_for_finalization: Whether to wait for finalization of the extrinsic. + + Returns: + bool: ``True`` if the batch unstaking is successful, False otherwise. + + This function allows for strategic reallocation or withdrawal of stakes, aligning with the dynamic stake + management aspect of the Bittensor network. + """ + return unstake_multiple_extrinsic( + subtensor=self, + wallet=wallet, + netuids=netuids, + hotkey_ss58s=hotkey_ss58s, + amounts=amounts, + unstake_all=unstake_all, + period=period, + raise_error=raise_error, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) diff --git a/bittensor/core/subtensor_api/__init__.py b/bittensor/core/subtensor_api/__init__.py new file mode 100644 index 0000000000..dbf261c341 --- /dev/null +++ b/bittensor/core/subtensor_api/__init__.py @@ -0,0 +1,253 @@ +from typing import Optional, Union, TYPE_CHECKING + +from bittensor.core.async_subtensor import AsyncSubtensor as _AsyncSubtensor +from bittensor.core.subtensor import Subtensor as _Subtensor +from .chain import Chain as _Chain +from .commitments import Commitments as _Commitments +from .delegates import Delegates as _Delegates +from .extrinsics import Extrinsics as _Extrinsics +from .metagraphs import Metagraphs as _Metagraphs +from .neurons import Neurons as _Neurons +from .queries import Queries as _Queries +from .staking import Staking as _Staking +from .subnets import Subnets as _Subnets +from .utils import add_legacy_methods as _add_classic_fields +from .wallets import Wallets as _Wallets + +if TYPE_CHECKING: + from bittensor.core.config import Config + + +class SubtensorApi: + """Subtensor API class. + + Arguments: + network: The network to connect to. Defaults to `None` -> "finney". + config: Bittensor configuration object. Defaults to `None`. + legacy_methods: If `True`, all methods from the Subtensor class will be added to the root level of this class. + fallback_endpoints: List of fallback endpoints to use if default or provided network is not available. Defaults to `None`. + retry_forever: Whether to retry forever on connection errors. Defaults to `False`. + log_verbose: Enables or disables verbose logging. + mock: Whether this is a mock instance. Mainly just for use in testing. + archive_endpoints: Similar to fallback_endpoints, but specifically only archive nodes. Will be used in cases + where you are requesting a block that is too old for your current (presumably lite) node. Defaults to `None` + websocket_shutdown_timer: Amount of time, in seconds, to wait after the last response from the chain to close + the connection. Only applicable to AsyncSubtensor. + + Example: + # sync version + import bittensor as bt + + subtensor = bt.SubtensorApi() + print(subtensor.block) + print(subtensor.delegates.get_delegate_identities()) + subtensor.chain.tx_rate_limit() + + # async version + import bittensor as bt + + subtensor = bt.SubtensorApi(async_subtensor=True) + async with subtensor: + print(await subtensor.block) + print(await subtensor.delegates.get_delegate_identities()) + print(await subtensor.chain.tx_rate_limit()) + + # using `legacy_methods` + import bittensor as bt + + subtensor = bt.SubtensorApi(legacy_methods=True) + print(subtensor.bonds(0)) + + # using `fallback_endpoints` or `retry_forever` + import bittensor as bt + + subtensor = bt.SubtensorApi( + network="finney", + fallback_endpoints=["wss://localhost:9945", "wss://some-other-endpoint:9945"], + retry_forever=True, + ) + print(subtensor.block) + """ + + def __init__( + self, + network: Optional[str] = None, + config: Optional["Config"] = None, + async_subtensor: bool = False, + legacy_methods: bool = False, + fallback_endpoints: Optional[list[str]] = None, + retry_forever: bool = False, + log_verbose: bool = False, + mock: bool = False, + archive_endpoints: Optional[list[str]] = None, + websocket_shutdown_timer: float = 5.0, + ): + self.network = network + self._fallback_endpoints = fallback_endpoints + self._archive_endpoints = archive_endpoints + self._retry_forever = retry_forever + self._ws_shutdown_timer = websocket_shutdown_timer + self._mock = mock + self.log_verbose = log_verbose + self.is_async = async_subtensor + self._config = config + + # assigned only for async instance + self.initialize = None + self._subtensor = self._get_subtensor() + + # fix naming collision + self._neurons = _Neurons(self._subtensor) + + # define empty fields + self.substrate = self._subtensor.substrate + self.chain_endpoint = self._subtensor.chain_endpoint + self.close = self._subtensor.close + self.config = self._subtensor.config + self.setup_config = self._subtensor.setup_config + self.help = self._subtensor.help + + self.determine_block_hash = self._subtensor.determine_block_hash + self.encode_params = self._subtensor.encode_params + self.sign_and_send_extrinsic = self._subtensor.sign_and_send_extrinsic + self.start_call = self._subtensor.start_call + self.wait_for_block = self._subtensor.wait_for_block + + # adds all Subtensor methods into main level os SubtensorApi class + if legacy_methods: + _add_classic_fields(self) + + def _get_subtensor(self) -> Union["_Subtensor", "_AsyncSubtensor"]: + """Returns the subtensor instance based on the provided config and subtensor type flag.""" + if self.is_async: + _subtensor = _AsyncSubtensor( + network=self.network, + config=self._config, + log_verbose=self.log_verbose, + fallback_endpoints=self._fallback_endpoints, + retry_forever=self._retry_forever, + _mock=self._mock, + archive_endpoints=self._archive_endpoints, + websocket_shutdown_timer=self._ws_shutdown_timer, + ) + self.initialize = _subtensor.initialize + return _subtensor + else: + return _Subtensor( + network=self.network, + config=self._config, + log_verbose=self.log_verbose, + fallback_endpoints=self._fallback_endpoints, + retry_forever=self._retry_forever, + _mock=self._mock, + archive_endpoints=self._archive_endpoints, + ) + + def _determine_chain_endpoint(self) -> str: + """Determines the connection and mock flag.""" + if self._mock: + return "Mock" + return self.substrate.url + + def __str__(self): + return ( + f"" + ) + + def __repr__(self): + return self.__str__() + + def __enter__(self): + if self.is_async: + raise NotImplementedError( + "Async version of SubtensorApi cannot be used with sync context manager." + ) + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + if self.is_async: + raise NotImplementedError( + "Async version of SubtensorApi cannot be used with sync context manager." + ) + self.close() + + async def __aenter__(self): + if not self.is_async: + raise NotImplementedError( + "Sync version of SubtensorApi cannot be used with async context manager." + ) + await self._subtensor.__aenter__() + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + if not self.is_async: + raise NotImplementedError( + "Sync version of SubtensorApi cannot be used with async context manager." + ) + await self.substrate.close() + + @classmethod + def add_args(cls, parser): + _Subtensor.add_args(parser) + + @property + def block(self): + """Returns current chain block number.""" + return self._subtensor.block + + @property + def chain(self): + """Property of interaction with chain methods.""" + return _Chain(self._subtensor) + + @property + def commitments(self): + """Property to access commitments methods.""" + return _Commitments(self._subtensor) + + @property + def delegates(self): + """Property to access delegates methods.""" + return _Delegates(self._subtensor) + + @property + def extrinsics(self): + """Property to access extrinsics methods.""" + return _Extrinsics(self._subtensor) + + @property + def metagraphs(self): + """Property to access metagraphs methods.""" + return _Metagraphs(self._subtensor) + + @property + def neurons(self): + """Property to access neurons methods.""" + return self._neurons + + @neurons.setter + def neurons(self, value): + """Setter for neurons property.""" + self._neurons = value + + @property + def queries(self): + """Property to access subtensor queries methods.""" + return _Queries(self._subtensor) + + @property + def staking(self): + """Property to access staking methods.""" + return _Staking(self._subtensor) + + @property + def subnets(self): + """Property of interaction with subnets methods.""" + return _Subnets(self._subtensor) + + @property + def wallets(self): + """Property of interaction methods with cold/hotkeys, and balances, etc.""" + return _Wallets(self._subtensor) diff --git a/bittensor/core/subtensor_api/chain.py b/bittensor/core/subtensor_api/chain.py new file mode 100644 index 0000000000..cd2bfda02f --- /dev/null +++ b/bittensor/core/subtensor_api/chain.py @@ -0,0 +1,20 @@ +from typing import Union +from bittensor.core.subtensor import Subtensor as _Subtensor +from bittensor.core.async_subtensor import AsyncSubtensor as _AsyncSubtensor + + +class Chain: + """Class for managing chain state operations.""" + + def __init__(self, subtensor: Union["_Subtensor", "_AsyncSubtensor"]): + self.get_block_hash = subtensor.get_block_hash + self.get_current_block = subtensor.get_current_block + self.get_delegate_identities = subtensor.get_delegate_identities + self.get_existential_deposit = subtensor.get_existential_deposit + self.get_minimum_required_stake = subtensor.get_minimum_required_stake + self.get_vote_data = subtensor.get_vote_data + self.get_timestamp = subtensor.get_timestamp + self.is_fast_blocks = subtensor.is_fast_blocks + self.last_drand_round = subtensor.last_drand_round + self.state_call = subtensor.state_call + self.tx_rate_limit = subtensor.tx_rate_limit diff --git a/bittensor/core/subtensor_api/commitments.py b/bittensor/core/subtensor_api/commitments.py new file mode 100644 index 0000000000..ff130a3e54 --- /dev/null +++ b/bittensor/core/subtensor_api/commitments.py @@ -0,0 +1,27 @@ +from typing import Union +from bittensor.core.subtensor import Subtensor as _Subtensor +from bittensor.core.async_subtensor import AsyncSubtensor as _AsyncSubtensor + + +class Commitments: + """Class for managing any commitment operations.""" + + def __init__(self, subtensor: Union["_Subtensor", "_AsyncSubtensor"]): + self.commit_reveal_enabled = subtensor.commit_reveal_enabled + self.get_all_commitments = subtensor.get_all_commitments + self.get_all_revealed_commitments = subtensor.get_all_revealed_commitments + self.get_commitment = subtensor.get_commitment + self.get_current_weight_commit_info = subtensor.get_current_weight_commit_info + self.get_current_weight_commit_info_v2 = ( + subtensor.get_current_weight_commit_info_v2 + ) + self.get_last_commitment_bonds_reset_block = ( + subtensor.get_last_commitment_bonds_reset_block + ) + self.get_revealed_commitment = subtensor.get_revealed_commitment + self.get_revealed_commitment_by_hotkey = ( + subtensor.get_revealed_commitment_by_hotkey + ) + self.get_timelocked_weight_commits = subtensor.get_timelocked_weight_commits + self.set_commitment = subtensor.set_commitment + self.set_reveal_commitment = subtensor.set_reveal_commitment diff --git a/bittensor/core/subtensor_api/delegates.py b/bittensor/core/subtensor_api/delegates.py new file mode 100644 index 0000000000..1cdbfca08c --- /dev/null +++ b/bittensor/core/subtensor_api/delegates.py @@ -0,0 +1,16 @@ +from typing import Union +from bittensor.core.subtensor import Subtensor as _Subtensor +from bittensor.core.async_subtensor import AsyncSubtensor as _AsyncSubtensor + + +class Delegates: + """Class for managing delegate operations.""" + + def __init__(self, subtensor: Union["_Subtensor", "_AsyncSubtensor"]): + self.is_hotkey_delegate = subtensor.is_hotkey_delegate + self.get_delegate_by_hotkey = subtensor.get_delegate_by_hotkey + self.set_delegate_take = subtensor.set_delegate_take + self.get_delegate_identities = subtensor.get_delegate_identities + self.get_delegate_take = subtensor.get_delegate_take + self.get_delegated = subtensor.get_delegated + self.get_delegates = subtensor.get_delegates diff --git a/bittensor/core/subtensor_api/extrinsics.py b/bittensor/core/subtensor_api/extrinsics.py new file mode 100644 index 0000000000..62c6c5c1b4 --- /dev/null +++ b/bittensor/core/subtensor_api/extrinsics.py @@ -0,0 +1,37 @@ +from typing import Union +from bittensor.core.subtensor import Subtensor as _Subtensor +from bittensor.core.async_subtensor import AsyncSubtensor as _AsyncSubtensor + + +class Extrinsics: + """Class for managing extrinsic operations.""" + + def __init__(self, subtensor: Union["_Subtensor", "_AsyncSubtensor"]): + self.add_liquidity = subtensor.add_liquidity + self.add_stake = subtensor.add_stake + self.add_stake_multiple = subtensor.add_stake_multiple + self.burned_register = subtensor.burned_register + self.commit_weights = subtensor.commit_weights + self.modify_liquidity = subtensor.modify_liquidity + self.move_stake = subtensor.move_stake + self.register = subtensor.register + self.register_subnet = subtensor.register_subnet + self.remove_liquidity = subtensor.remove_liquidity + self.reveal_weights = subtensor.reveal_weights + self.root_register = subtensor.root_register + self.root_set_pending_childkey_cooldown = ( + subtensor.root_set_pending_childkey_cooldown + ) + self.set_children = subtensor.set_children + self.set_subnet_identity = subtensor.set_subnet_identity + self.set_weights = subtensor.set_weights + self.serve_axon = subtensor.serve_axon + self.set_commitment = subtensor.set_commitment + self.start_call = subtensor.start_call + self.swap_stake = subtensor.swap_stake + self.toggle_user_liquidity = subtensor.toggle_user_liquidity + self.transfer = subtensor.transfer + self.transfer_stake = subtensor.transfer_stake + self.unstake = subtensor.unstake + self.unstake_all = subtensor.unstake_all + self.unstake_multiple = subtensor.unstake_multiple diff --git a/bittensor/core/subtensor_api/metagraphs.py b/bittensor/core/subtensor_api/metagraphs.py new file mode 100644 index 0000000000..af143a1620 --- /dev/null +++ b/bittensor/core/subtensor_api/metagraphs.py @@ -0,0 +1,12 @@ +from typing import Union +from bittensor.core.subtensor import Subtensor as _Subtensor +from bittensor.core.async_subtensor import AsyncSubtensor as _AsyncSubtensor + + +class Metagraphs: + """Class for managing metagraph operations.""" + + def __init__(self, subtensor: Union["_Subtensor", "_AsyncSubtensor"]): + self.get_metagraph_info = subtensor.get_metagraph_info + self.get_all_metagraphs_info = subtensor.get_all_metagraphs_info + self.metagraph = subtensor.metagraph diff --git a/bittensor/core/subtensor_api/neurons.py b/bittensor/core/subtensor_api/neurons.py new file mode 100644 index 0000000000..c1fd40066f --- /dev/null +++ b/bittensor/core/subtensor_api/neurons.py @@ -0,0 +1,15 @@ +from typing import Union +from bittensor.core.subtensor import Subtensor as _Subtensor +from bittensor.core.async_subtensor import AsyncSubtensor as _AsyncSubtensor + + +class Neurons: + """Class for managing neuron operations.""" + + def __init__(self, subtensor: Union["_Subtensor", "_AsyncSubtensor"]): + self.get_all_neuron_certificates = subtensor.get_all_neuron_certificates + self.get_neuron_certificate = subtensor.get_neuron_certificate + self.neuron_for_uid = subtensor.neuron_for_uid + self.neurons = subtensor.neurons + self.neurons_lite = subtensor.neurons_lite + self.query_identity = subtensor.query_identity diff --git a/bittensor/core/subtensor_api/queries.py b/bittensor/core/subtensor_api/queries.py new file mode 100644 index 0000000000..7209ffb7ae --- /dev/null +++ b/bittensor/core/subtensor_api/queries.py @@ -0,0 +1,15 @@ +from typing import Union +from bittensor.core.subtensor import Subtensor as _Subtensor +from bittensor.core.async_subtensor import AsyncSubtensor as _AsyncSubtensor + + +class Queries: + """Class for managing subtensor query operations.""" + + def __init__(self, subtensor: Union["_Subtensor", "_AsyncSubtensor"]): + self.query_constant = subtensor.query_constant + self.query_map = subtensor.query_map + self.query_map_subtensor = subtensor.query_map_subtensor + self.query_module = subtensor.query_module + self.query_runtime_api = subtensor.query_runtime_api + self.query_subtensor = subtensor.query_subtensor diff --git a/bittensor/core/subtensor_api/staking.py b/bittensor/core/subtensor_api/staking.py new file mode 100644 index 0000000000..a85d88250e --- /dev/null +++ b/bittensor/core/subtensor_api/staking.py @@ -0,0 +1,30 @@ +from typing import Union +from bittensor.core.subtensor import Subtensor as _Subtensor +from bittensor.core.async_subtensor import AsyncSubtensor as _AsyncSubtensor + + +class Staking: + """Class for managing staking operations.""" + + def __init__(self, subtensor: Union["_Subtensor", "_AsyncSubtensor"]): + self.add_stake = subtensor.add_stake + self.add_stake_multiple = subtensor.add_stake_multiple + self.get_hotkey_stake = subtensor.get_hotkey_stake + self.get_minimum_required_stake = subtensor.get_minimum_required_stake + self.get_stake = subtensor.get_stake + self.get_stake_add_fee = subtensor.get_stake_add_fee + self.get_stake_for_coldkey = subtensor.get_stake_for_coldkey + self.get_stake_for_coldkey_and_hotkey = ( + subtensor.get_stake_for_coldkey_and_hotkey + ) + self.get_stake_info_for_coldkey = subtensor.get_stake_info_for_coldkey + self.get_stake_movement_fee = subtensor.get_stake_movement_fee + self.get_stake_operations_fee = subtensor.get_stake_operations_fee + self.get_stake_weight = subtensor.get_stake_weight + self.get_unstake_fee = subtensor.get_unstake_fee + self.move_stake = subtensor.move_stake + self.swap_stake = subtensor.swap_stake + self.transfer_stake = subtensor.transfer_stake + self.unstake = subtensor.unstake + self.unstake_all = subtensor.unstake_all + self.unstake_multiple = subtensor.unstake_multiple diff --git a/bittensor/core/subtensor_api/subnets.py b/bittensor/core/subtensor_api/subnets.py new file mode 100644 index 0000000000..6106f2e04f --- /dev/null +++ b/bittensor/core/subtensor_api/subnets.py @@ -0,0 +1,53 @@ +from typing import Union + +from bittensor.core.async_subtensor import AsyncSubtensor as _AsyncSubtensor +from bittensor.core.subtensor import Subtensor as _Subtensor + + +class Subnets: + """Class for managing subnet operations.""" + + def __init__(self, subtensor: Union["_Subtensor", "_AsyncSubtensor"]): + self.all_subnets = subtensor.all_subnets + self.blocks_since_last_step = subtensor.blocks_since_last_step + self.blocks_since_last_update = subtensor.blocks_since_last_update + self.bonds = subtensor.bonds + self.burned_register = subtensor.burned_register + self.commit_reveal_enabled = subtensor.commit_reveal_enabled + self.difficulty = subtensor.difficulty + self.get_all_subnets_info = subtensor.get_all_subnets_info + self.get_parents = subtensor.get_parents + self.get_children = subtensor.get_children + self.get_children_pending = subtensor.get_children_pending + self.get_current_weight_commit_info = subtensor.get_current_weight_commit_info + self.get_hyperparameter = subtensor.get_hyperparameter + self.get_liquidity_list = subtensor.get_liquidity_list + self.get_neuron_for_pubkey_and_subnet = ( + subtensor.get_neuron_for_pubkey_and_subnet + ) + self.get_next_epoch_start_block = subtensor.get_next_epoch_start_block + self.get_subnet_burn_cost = subtensor.get_subnet_burn_cost + self.get_subnet_hyperparameters = subtensor.get_subnet_hyperparameters + self.get_subnet_info = subtensor.get_subnet_info + self.get_subnet_price = subtensor.get_subnet_price + self.get_subnet_prices = subtensor.get_subnet_prices + self.get_subnet_owner_hotkey = subtensor.get_subnet_owner_hotkey + self.get_subnet_reveal_period_epochs = subtensor.get_subnet_reveal_period_epochs + self.get_subnet_validator_permits = subtensor.get_subnet_validator_permits + self.get_subnets = subtensor.get_subnets + self.get_total_subnets = subtensor.get_total_subnets + self.get_uid_for_hotkey_on_subnet = subtensor.get_uid_for_hotkey_on_subnet + self.immunity_period = subtensor.immunity_period + self.is_hotkey_registered_on_subnet = subtensor.is_hotkey_registered_on_subnet + self.is_subnet_active = subtensor.is_subnet_active + self.max_weight_limit = subtensor.max_weight_limit + self.min_allowed_weights = subtensor.min_allowed_weights + self.recycle = subtensor.recycle + self.register_subnet = subtensor.register_subnet + self.set_subnet_identity = subtensor.set_subnet_identity + self.subnet = subtensor.subnet + self.subnet_exists = subtensor.subnet_exists + self.subnetwork_n = subtensor.subnetwork_n + self.tempo = subtensor.tempo + self.weights_rate_limit = subtensor.weights_rate_limit + self.weights = subtensor.weights diff --git a/bittensor/core/subtensor_api/utils.py b/bittensor/core/subtensor_api/utils.py new file mode 100644 index 0000000000..34cc48502a --- /dev/null +++ b/bittensor/core/subtensor_api/utils.py @@ -0,0 +1,181 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from bittensor.core.subtensor_api import SubtensorApi + + +def add_legacy_methods(subtensor: "SubtensorApi"): + """If SubtensorApi get `subtensor_fields=True` arguments, then all classic Subtensor fields added to root level.""" + subtensor.add_liquidity = subtensor._subtensor.add_liquidity + subtensor.add_stake = subtensor._subtensor.add_stake + subtensor.add_stake_multiple = subtensor._subtensor.add_stake_multiple + subtensor.all_subnets = subtensor._subtensor.all_subnets + subtensor.blocks_since_last_step = subtensor._subtensor.blocks_since_last_step + subtensor.blocks_since_last_update = subtensor._subtensor.blocks_since_last_update + subtensor.bonds = subtensor._subtensor.bonds + subtensor.burned_register = subtensor._subtensor.burned_register + subtensor.chain_endpoint = subtensor._subtensor.chain_endpoint + subtensor.commit_reveal_enabled = subtensor._subtensor.commit_reveal_enabled + subtensor.commit_weights = subtensor._subtensor.commit_weights + subtensor.determine_block_hash = subtensor._subtensor.determine_block_hash + subtensor.difficulty = subtensor._subtensor.difficulty + subtensor.does_hotkey_exist = subtensor._subtensor.does_hotkey_exist + subtensor.encode_params = subtensor._subtensor.encode_params + subtensor.filter_netuids_by_registered_hotkeys = ( + subtensor._subtensor.filter_netuids_by_registered_hotkeys + ) + subtensor.get_all_commitments = subtensor._subtensor.get_all_commitments + subtensor.get_all_metagraphs_info = subtensor._subtensor.get_all_metagraphs_info + subtensor.get_all_neuron_certificates = ( + subtensor._subtensor.get_all_neuron_certificates + ) + subtensor.get_all_revealed_commitments = ( + subtensor._subtensor.get_all_revealed_commitments + ) + subtensor.get_all_subnets_info = subtensor._subtensor.get_all_subnets_info + subtensor.get_balance = subtensor._subtensor.get_balance + subtensor.get_balances = subtensor._subtensor.get_balances + subtensor.get_block_hash = subtensor._subtensor.get_block_hash + subtensor.get_parents = subtensor._subtensor.get_parents + subtensor.get_children = subtensor._subtensor.get_children + subtensor.get_children_pending = subtensor._subtensor.get_children_pending + subtensor.get_commitment = subtensor._subtensor.get_commitment + subtensor.get_current_block = subtensor._subtensor.get_current_block + subtensor.get_last_commitment_bonds_reset_block = ( + subtensor._subtensor.get_last_commitment_bonds_reset_block + ) + subtensor.get_current_weight_commit_info = ( + subtensor._subtensor.get_current_weight_commit_info + ) + subtensor.get_current_weight_commit_info_v2 = ( + subtensor._subtensor.get_current_weight_commit_info_v2 + ) + subtensor.get_delegate_by_hotkey = subtensor._subtensor.get_delegate_by_hotkey + subtensor.get_delegate_identities = subtensor._subtensor.get_delegate_identities + subtensor.get_delegate_take = subtensor._subtensor.get_delegate_take + subtensor.get_delegated = subtensor._subtensor.get_delegated + subtensor.get_delegates = subtensor._subtensor.get_delegates + subtensor.get_existential_deposit = subtensor._subtensor.get_existential_deposit + subtensor.get_hotkey_owner = subtensor._subtensor.get_hotkey_owner + subtensor.get_hotkey_stake = subtensor._subtensor.get_hotkey_stake + subtensor.get_hyperparameter = subtensor._subtensor.get_hyperparameter + subtensor.get_liquidity_list = subtensor._subtensor.get_liquidity_list + subtensor.get_metagraph_info = subtensor._subtensor.get_metagraph_info + subtensor.get_minimum_required_stake = ( + subtensor._subtensor.get_minimum_required_stake + ) + subtensor.get_netuids_for_hotkey = subtensor._subtensor.get_netuids_for_hotkey + subtensor.get_neuron_certificate = subtensor._subtensor.get_neuron_certificate + subtensor.get_neuron_for_pubkey_and_subnet = ( + subtensor._subtensor.get_neuron_for_pubkey_and_subnet + ) + subtensor.get_next_epoch_start_block = ( + subtensor._subtensor.get_next_epoch_start_block + ) + subtensor.get_owned_hotkeys = subtensor._subtensor.get_owned_hotkeys + subtensor.get_revealed_commitment = subtensor._subtensor.get_revealed_commitment + subtensor.get_revealed_commitment_by_hotkey = ( + subtensor._subtensor.get_revealed_commitment_by_hotkey + ) + subtensor.get_stake = subtensor._subtensor.get_stake + subtensor.get_stake_add_fee = subtensor._subtensor.get_stake_add_fee + subtensor.get_stake_for_coldkey = subtensor._subtensor.get_stake_for_coldkey + subtensor.get_stake_for_coldkey_and_hotkey = ( + subtensor._subtensor.get_stake_for_coldkey_and_hotkey + ) + subtensor.get_stake_for_hotkey = subtensor._subtensor.get_stake_for_hotkey + subtensor.get_stake_info_for_coldkey = ( + subtensor._subtensor.get_stake_info_for_coldkey + ) + subtensor.get_stake_movement_fee = subtensor._subtensor.get_stake_movement_fee + subtensor.get_stake_operations_fee = subtensor._subtensor.get_stake_operations_fee + subtensor.get_stake_weight = subtensor._subtensor.get_stake_weight + subtensor.get_subnet_burn_cost = subtensor._subtensor.get_subnet_burn_cost + subtensor.get_subnet_hyperparameters = ( + subtensor._subtensor.get_subnet_hyperparameters + ) + subtensor.get_subnet_info = subtensor._subtensor.get_subnet_info + subtensor.get_subnet_price = subtensor._subtensor.get_subnet_price + subtensor.get_subnet_prices = subtensor._subtensor.get_subnet_prices + subtensor.get_subnet_owner_hotkey = subtensor._subtensor.get_subnet_owner_hotkey + subtensor.get_subnet_reveal_period_epochs = ( + subtensor._subtensor.get_subnet_reveal_period_epochs + ) + subtensor.get_subnet_validator_permits = ( + subtensor._subtensor.get_subnet_validator_permits + ) + subtensor.get_subnets = subtensor._subtensor.get_subnets + subtensor.get_timelocked_weight_commits = ( + subtensor._subtensor.get_timelocked_weight_commits + ) + subtensor.get_timestamp = subtensor._subtensor.get_timestamp + subtensor.get_total_subnets = subtensor._subtensor.get_total_subnets + subtensor.get_transfer_fee = subtensor._subtensor.get_transfer_fee + subtensor.get_uid_for_hotkey_on_subnet = ( + subtensor._subtensor.get_uid_for_hotkey_on_subnet + ) + subtensor.get_unstake_fee = subtensor._subtensor.get_unstake_fee + subtensor.get_vote_data = subtensor._subtensor.get_vote_data + subtensor.immunity_period = subtensor._subtensor.immunity_period + subtensor.is_fast_blocks = subtensor._subtensor.is_fast_blocks + subtensor.is_hotkey_delegate = subtensor._subtensor.is_hotkey_delegate + subtensor.is_hotkey_registered = subtensor._subtensor.is_hotkey_registered + subtensor.is_hotkey_registered_any = subtensor._subtensor.is_hotkey_registered_any + subtensor.is_hotkey_registered_on_subnet = ( + subtensor._subtensor.is_hotkey_registered_on_subnet + ) + subtensor.is_subnet_active = subtensor._subtensor.is_subnet_active + subtensor.last_drand_round = subtensor._subtensor.last_drand_round + subtensor.log_verbose = subtensor._subtensor.log_verbose + subtensor.max_weight_limit = subtensor._subtensor.max_weight_limit + subtensor.metagraph = subtensor._subtensor.metagraph + subtensor.min_allowed_weights = subtensor._subtensor.min_allowed_weights + subtensor.modify_liquidity = subtensor._subtensor.modify_liquidity + subtensor.move_stake = subtensor._subtensor.move_stake + subtensor.network = subtensor._subtensor.network + subtensor.neurons = subtensor._subtensor.neurons + subtensor.neuron_for_uid = subtensor._subtensor.neuron_for_uid + subtensor.neurons_lite = subtensor._subtensor.neurons_lite + subtensor.query_constant = subtensor._subtensor.query_constant + subtensor.query_identity = subtensor._subtensor.query_identity + subtensor.query_map = subtensor._subtensor.query_map + subtensor.query_map_subtensor = subtensor._subtensor.query_map_subtensor + subtensor.query_module = subtensor._subtensor.query_module + subtensor.query_runtime_api = subtensor._subtensor.query_runtime_api + subtensor.query_subtensor = subtensor._subtensor.query_subtensor + subtensor.recycle = subtensor._subtensor.recycle + subtensor.remove_liquidity = subtensor._subtensor.remove_liquidity + subtensor.register = subtensor._subtensor.register + subtensor.register_subnet = subtensor._subtensor.register_subnet + subtensor.reveal_weights = subtensor._subtensor.reveal_weights + subtensor.root_register = subtensor._subtensor.root_register + subtensor.root_set_pending_childkey_cooldown = ( + subtensor._subtensor.root_set_pending_childkey_cooldown + ) + subtensor.serve_axon = subtensor._subtensor.serve_axon + subtensor.set_children = subtensor._subtensor.set_children + subtensor.set_commitment = subtensor._subtensor.set_commitment + subtensor.set_delegate_take = subtensor._subtensor.set_delegate_take + subtensor.set_reveal_commitment = subtensor._subtensor.set_reveal_commitment + subtensor.set_subnet_identity = subtensor._subtensor.set_subnet_identity + subtensor.set_weights = subtensor._subtensor.set_weights + subtensor.setup_config = subtensor._subtensor.setup_config + subtensor.sign_and_send_extrinsic = subtensor._subtensor.sign_and_send_extrinsic + subtensor.start_call = subtensor._subtensor.start_call + subtensor.state_call = subtensor._subtensor.state_call + subtensor.subnet = subtensor._subtensor.subnet + subtensor.subnet_exists = subtensor._subtensor.subnet_exists + subtensor.subnetwork_n = subtensor._subtensor.subnetwork_n + subtensor.substrate = subtensor._subtensor.substrate + subtensor.swap_stake = subtensor._subtensor.swap_stake + subtensor.tempo = subtensor._subtensor.tempo + subtensor.toggle_user_liquidity = subtensor._subtensor.toggle_user_liquidity + subtensor.transfer = subtensor._subtensor.transfer + subtensor.transfer_stake = subtensor._subtensor.transfer_stake + subtensor.tx_rate_limit = subtensor._subtensor.tx_rate_limit + subtensor.unstake = subtensor._subtensor.unstake + subtensor.unstake_all = subtensor._subtensor.unstake_all + subtensor.unstake_multiple = subtensor._subtensor.unstake_multiple + subtensor.wait_for_block = subtensor._subtensor.wait_for_block + subtensor.weights = subtensor._subtensor.weights + subtensor.weights_rate_limit = subtensor._subtensor.weights_rate_limit diff --git a/bittensor/core/subtensor_api/wallets.py b/bittensor/core/subtensor_api/wallets.py new file mode 100644 index 0000000000..9b3a3a058b --- /dev/null +++ b/bittensor/core/subtensor_api/wallets.py @@ -0,0 +1,41 @@ +from typing import Union +from bittensor.core.subtensor import Subtensor as _Subtensor +from bittensor.core.async_subtensor import AsyncSubtensor as _AsyncSubtensor + + +class Wallets: + """Class for managing coldkey, hotkey, wallet operations.""" + + def __init__(self, subtensor: Union["_Subtensor", "_AsyncSubtensor"]): + self.does_hotkey_exist = subtensor.does_hotkey_exist + self.filter_netuids_by_registered_hotkeys = ( + subtensor.filter_netuids_by_registered_hotkeys + ) + self.is_hotkey_registered_any = subtensor.is_hotkey_registered_any + self.is_hotkey_registered = subtensor.is_hotkey_registered + self.is_hotkey_registered_on_subnet = subtensor.is_hotkey_registered_on_subnet + self.is_hotkey_delegate = subtensor.is_hotkey_delegate + self.get_balance = subtensor.get_balance + self.get_balances = subtensor.get_balances + self.get_children = subtensor.get_children + self.get_children_pending = subtensor.get_children_pending + self.get_delegate_by_hotkey = subtensor.get_delegate_by_hotkey + self.get_delegate_take = subtensor.get_delegate_take + self.get_delegated = subtensor.get_delegated + self.get_hotkey_owner = subtensor.get_hotkey_owner + self.get_hotkey_stake = subtensor.get_hotkey_stake + self.get_minimum_required_stake = subtensor.get_minimum_required_stake + self.get_netuids_for_hotkey = subtensor.get_netuids_for_hotkey + self.get_owned_hotkeys = subtensor.get_owned_hotkeys + self.get_parents = subtensor.get_parents + self.get_stake = subtensor.get_stake + self.get_stake_add_fee = subtensor.get_stake_add_fee + self.get_stake_for_coldkey = subtensor.get_stake_for_coldkey + self.get_stake_for_coldkey_and_hotkey = ( + subtensor.get_stake_for_coldkey_and_hotkey + ) + self.get_stake_for_hotkey = subtensor.get_stake_for_hotkey + self.get_stake_info_for_coldkey = subtensor.get_stake_info_for_coldkey + self.get_stake_movement_fee = subtensor.get_stake_movement_fee + self.get_transfer_fee = subtensor.get_transfer_fee + self.get_unstake_fee = subtensor.get_unstake_fee diff --git a/bittensor/core/synapse.py b/bittensor/core/synapse.py new file mode 100644 index 0000000000..aa7abbbffe --- /dev/null +++ b/bittensor/core/synapse.py @@ -0,0 +1,857 @@ +import base64 +import json +import sys +import warnings +from typing import cast, Any, ClassVar, Optional, Union + +from pydantic import ( + BaseModel, + ConfigDict, + Field, + field_validator, + model_validator, +) + +from bittensor.utils import get_hash +from bittensor.utils.btlogging import logging + + +def get_size(obj: Any, seen: Optional[set] = None) -> int: + """ + Recursively finds size of objects. + + This function traverses every item of a given object and sums their sizes to compute the total size. + + Args: + obj (Any): The object to get the size of. + seen (Optional[set]): Set of object ids that have been calculated. + + Returns: + int: The total size of the object. + + """ + size = sys.getsizeof(obj) + if seen is None: + seen = set() + obj_id = id(obj) + if obj_id in seen: + return 0 + # Important mark as seen *before* entering recursion to gracefully handle + # self-referential objects + seen.add(obj_id) + if isinstance(obj, dict): + size += sum([get_size(v, seen) for v in obj.values()]) + size += sum([get_size(k, seen) for k in obj.keys()]) + elif hasattr(obj, "__dict__"): + size += get_size(obj.__dict__, seen) + elif hasattr(obj, "__iter__") and not isinstance(obj, (str, bytes, bytearray)): + size += sum([get_size(i, seen) for i in obj]) + return size + + +def cast_int(raw: str) -> int: + """ + Converts a string to an integer, if the string is not ``None``. + + This function attempts to convert a string to an integer. If the string is ``None``, it simply returns ``None``. + + Args: + raw (str): The string to convert. + + Returns: + int or None: The converted integer, or ``None`` if the input was ``None``. + + """ + return int(raw) if raw is not None else raw + + +def cast_float(raw: str) -> Optional[float]: + """ + Converts a string to a float, if the string is not ``None``. + + This function attempts to convert a string to a float. If the string is ``None``, it simply returns ``None``. + + Args: + raw (str): The string to convert. + + Returns: + float or None: The converted float, or ``None`` if the input was ``None``. + + """ + return float(raw) if raw is not None else raw + + +class TerminalInfo(BaseModel): + """ + TerminalInfo encapsulates detailed information about a network synapse (node) involved in a communication process. + + This class serves as a metadata carrier, + providing essential details about the state and configuration of a terminal during network interactions. This is a + crucial class in the Bittensor framework. + + The TerminalInfo class contains information such as HTTP status codes and messages, processing times, + IP addresses, ports, Bittensor version numbers, and unique identifiers. These details are vital for + maintaining network reliability, security, and efficient data flow within the Bittensor network. + + This class includes Pydantic validators and root validators to enforce data integrity and format. It is + designed to be used natively within Synapses, so that you will not need to call this directly, but rather + is used as a helper class for Synapses. + + Args: + status_code (int): HTTP status code indicating the result of a network request. Essential for identifying the + outcome of network interactions. + status_message (str): Descriptive message associated with the status code, providing additional context about + the request's result. + process_time (float): Time taken by the terminal to process the call, important for performance monitoring and + optimization. + ip (str): IP address of the terminal, crucial for network routing and data transmission. + port (int): Network port used by the terminal, key for establishing network connections. + version (int): Bittensor version running on the terminal, ensuring compatibility between different nodes in the + network. + nonce (int): Unique, monotonically increasing number for each terminal, aiding in identifying and ordering + network interactions. + uuid (str): Unique identifier for the terminal, fundamental for network security and identification. + hotkey (str): Encoded hotkey string of the terminal wallet, important for transaction and identity verification + in the network. + signature (str): Digital signature verifying the tuple of nonce, axon_hotkey, dendrite_hotkey, and uuid, + critical for ensuring data authenticity and security. + + Usage:: + + # Creating a TerminalInfo instance + from bittensor.core.synapse import TerminalInfo + + terminal_info = TerminalInfo( + status_code=200, + status_message="Success", + process_time=0.1, + ip="198.123.23.1", + port=9282, + version=111, + nonce=111111, + uuid="5ecbd69c-1cec-11ee-b0dc-e29ce36fec1a", + hotkey="5EnjDGNqqWnuL2HCAdxeEtN2oqtXZw6BMBe936Kfy2PFz1J1", + signature="0x0813029319030129u4120u10841824y0182u091u230912u" + ) + + # Accessing TerminalInfo attributes + ip_address = terminal_info.ip + processing_duration = terminal_info.process_time + + # TerminalInfo can be used to monitor and verify network interactions, ensuring proper communication and + security within the Bittensor network. + + TerminalInfo plays a pivotal role in providing transparency and control over network operations, making it an + indispensable tool for developers and users interacting with the Bittensor ecosystem. + """ + + model_config = ConfigDict(validate_assignment=True) + + # The HTTP status code from: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status + status_code: Optional[int] = Field( + title="status_code", + description="The HTTP status code from: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status", + examples=[200], + default=None, + frozen=False, + ) + + # The HTTP status code from: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status + status_message: Optional[str] = Field( + title="status_message", + description="The status_message associated with the status_code", + examples=["Success"], + default=None, + frozen=False, + ) + + # Process time on this terminal side of call + process_time: Optional[float] = Field( + title="process_time", + description="Process time on this terminal side of call", + examples=[0.1], + default=None, + frozen=False, + ) + + # The terminal ip. + ip: Optional[str] = Field( + title="ip", + description="The ip of the axon receiving the request.", + examples=["198.123.23.1"], + default=None, + frozen=False, + ) + + # The host port of the terminal. + port: Optional[int] = Field( + title="port", + description="The port of the terminal.", + examples=["9282"], + default=None, + frozen=False, + ) + + # The bittensor version on the terminal as an int. + version: Optional[int] = Field( + title="version", + description="The bittensor version on the axon as str(int)", + examples=[111], + default=None, + frozen=False, + ) + + # A Unix timestamp to associate with the terminal + nonce: Optional[int] = Field( + title="nonce", + description="A Unix timestamp that prevents replay attacks", + examples=[111111], + default=None, + frozen=False, + ) + + # A unique identifier associated with the terminal, set on the axon side. + uuid: Optional[str] = Field( + title="uuid", + description="A unique identifier associated with the terminal", + examples=["5ecbd69c-1cec-11ee-b0dc-e29ce36fec1a"], + default=None, + frozen=False, + ) + + # The bittensor version on the terminal as an int. + hotkey: Optional[str] = Field( + title="hotkey", + description="The ss58 encoded hotkey string of the terminal wallet.", + examples=["5EnjDGNqqWnuL2HCAdxeEtN2oqtXZw6BMBe936Kfy2PFz1J1"], + default=None, + frozen=False, + ) + + # A signature verifying the tuple (axon_nonce, axon_hotkey, dendrite_hotkey, axon_uuid) + signature: Optional[str] = Field( + title="signature", + description="A signature verifying the tuple (nonce, axon_hotkey, dendrite_hotkey, uuid)", + examples=["0x0813029319030129u4120u10841824y0182u091u230912u"], + default=None, + frozen=False, + ) + + # Extract the process time on this terminal side of call as a float + _extract_process_time = field_validator("process_time", mode="before")(cast_float) + + # Extract the host port of the terminal as an int + _extract_port = field_validator("port", mode="before")(cast_int) + + # Extract the bittensor version on the terminal as an int. + _extract_version = field_validator("version", mode="before")(cast_int) + + # Extract the Unix timestamp associated with the terminal as an int + _extract_nonce = field_validator("nonce", mode="before")(cast_int) + + # Extract the HTTP status code as an int + _extract_status_code = field_validator("status_code", mode="before")(cast_int) + + +class Synapse(BaseModel): + """ + Represents a Synapse in the Bittensor network, serving as a communication schema between neurons (nodes). + + Synapses ensure the format and correctness of transmission tensors according to the Bittensor protocol. + Each Synapse type is tailored for a specific machine learning (ML) task, following unique compression and + communication processes. This helps maintain sanitized, correct, and useful information flow across the network. + + The Synapse class encompasses essential network properties such as HTTP route names, timeouts, request sizes, and + terminal information. It also includes methods for serialization, deserialization, attribute setting, and hash + computation, ensuring secure and efficient data exchange in the network. + + The class includes Pydantic validators and root validators to enforce data integrity and format. Additionally, + properties like ``is_success``, ``is_failure``, ``is_timeout``, etc., provide convenient status checks based on + dendrite responses. + + Think of Bittensor Synapses as glorified pydantic wrappers that have been designed to be used in a distributed + network. They provide a standardized way to communicate between neurons, and are the primary mechanism for + communication between neurons in Bittensor. + + Key Features: + + 1. HTTP Route Name (``name`` attribute): + Enables the identification and proper routing of requests within the network. Essential for users + defining custom routes for specific machine learning tasks. + + 2. Query Timeout (``timeout`` attribute): + Determines the maximum duration allowed for a query, ensuring timely responses and network + efficiency. Crucial for users to manage network latency and response times, particularly in + time-sensitive applications. + + 3. Request Sizes (``total_size``, ``header_size`` attributes): + Keeps track of the size of request bodies and headers, ensuring efficient data transmission without + overloading the network. Important for users to monitor and optimize the data payload, especially + in bandwidth-constrained environments. + + 4. Terminal Information (``dendrite``, ``axon`` attributes): + Stores information about the dendrite (receiving end) and axon (sending end), facilitating communication + between nodes. Users can access detailed information about the communication endpoints, aiding in + debugging and network analysis. + + 5. Body Hash Computation (``computed_body_hash``, ``required_hash_fields``): + Ensures data integrity and security by computing hashes of transmitted data. Provides users with a + mechanism to verify data integrity and detect any tampering during transmission. + It is recommended that names of fields in `required_hash_fields` are listed in the order they are + defined in the class. + + 6. Serialization and Deserialization Methods: + Facilitates the conversion of Synapse objects to and from a format suitable for network transmission. + Essential for users who need to customize data formats for specific machine learning models or tasks. + + 7. Status Check Properties (``is_success``, ``is_failure``, ``is_timeout``, etc.): + Provides quick and easy methods to check the status of a request, improving error handling and + response management. Users can efficiently handle different outcomes of network requests, enhancing + the robustness of their applications. + + Example usage:: + + # Creating a Synapse instance with default values + from bittensor.core.synapse import Synapse + + synapse = Synapse() + + # Setting properties and input + synapse.timeout = 15.0 + synapse.name = "MySynapse" + + # Not setting fields that are not defined in your synapse class will result in an error, e.g.: + synapse.dummy_input = 1 # This will raise an error because dummy_input is not defined in the Synapse class + + # Get a dictionary of headers and body from the synapse instance + synapse_dict = synapse.model_dump_json() + + # Get a dictionary of headers from the synapse instance + headers = synapse.to_headers() + + # Reconstruct the synapse from headers using the classmethod 'from_headers' + synapse = Synapse.from_headers(headers) + + # Deserialize synapse after receiving it over the network, controlled by `deserialize` method + deserialized_synapse = synapse.deserialize() + + # Checking the status of the request + if synapse.is_success: + print("Request succeeded") + + # Checking and setting the status of the request + print(synapse.axon.status_code) + synapse.axon.status_code = 408 # Timeout + + Args: + name (str): HTTP route name, set on :func:`axon.attach`. + timeout (float): Total query length, set by the dendrite terminal. + total_size (int): Total size of request body in bytes. + header_size (int): Size of request header in bytes. + dendrite (:func:`TerminalInfo`): Information about the dendrite terminal. + axon (:func:`TerminalInfo`): Information about the axon terminal. + computed_body_hash (str): Computed hash of the request body. + required_hash_fields (list[str]): Fields required to compute the body hash. + + Methods: + deserialize: Custom deserialization logic for subclasses. + __setattr__: Override method to make ``required_hash_fields`` read-only. + get_total_size: Calculates and returns the total size of the object. + to_headers: Constructs a dictionary of headers from instance properties. + body_hash: Computes a SHA3-256 hash of the serialized body. + parse_headers_to_inputs: Parses headers to construct an inputs dictionary. + from_headers: Creates an instance from a headers dictionary. + + This class is a cornerstone in the Bittensor framework, providing the necessary tools for secure, efficient, and + standardized communication in a decentralized environment. + """ + + model_config = ConfigDict(validate_assignment=True) + + def deserialize(self) -> "Synapse": + """ + Deserializes the Synapse object. + + This method is intended to be overridden by subclasses for custom deserialization logic. + In the context of the Synapse superclass, this method simply returns the instance itself. + When inheriting from this class, subclasses should provide their own implementation for + deserialization if specific deserialization behavior is desired. + + By default, if a subclass does not provide its own implementation of this method, the + Synapse's deserialize method will be used, returning the object instance as-is. + + In its default form, this method simply returns the instance of the Synapse itself without any modifications. + Subclasses of Synapse can override this method to add specific deserialization behaviors, such as converting + serialized data back into complex object types or performing additional data integrity checks. + + Example:: + + class CustomSynapse(Synapse): + additional_data: str + + def deserialize(self) -> "CustomSynapse": + # Custom deserialization logic + # For example, decoding a base64 encoded string in 'additional_data' + if self.additional_data: + self.additional_data = base64.b64decode(self.additional_data).decode('utf-8') + return self + + serialized_data = '{"additional_data": "SGVsbG8gV29ybGQ="}' # Base64 for 'Hello World' + custom_synapse = CustomSynapse.model_validate_json(serialized_data) + deserialized_synapse = custom_synapse.deserialize() + + # deserialized_synapse.additional_data would now be 'Hello World' + + Returns: + Synapse: The deserialized Synapse object. In this default implementation, it returns the object itself. + """ + return self + + @model_validator(mode="before") + def set_name_type(cls, values: dict) -> dict: + values["name"] = cls.__name__ # type: ignore + return values + + # Defines the http route name which is set on axon.attach( callable( request: RequestName )) + name: Optional[str] = Field( + title="name", + description="Defines the http route name which is set on axon.attach( callable( request: RequestName ))", + examples=["Forward"], + frozen=False, + default=None, + repr=False, + ) + + # The call timeout, set by the dendrite terminal. + timeout: Optional[float] = Field( + title="timeout", + description="Defines the total query length.", + examples=[12.0], + default=12.0, + frozen=False, + repr=False, + ) + + # The call timeout, set by the dendrite terminal. + total_size: Optional[int] = Field( + title="total_size", + description="Total size of request body in bytes.", + examples=[1000], + default=0, + frozen=False, + repr=False, + ) + + # The call timeout, set by the dendrite terminal. + header_size: Optional[int] = Field( + title="header_size", + description="Size of request header in bytes.", + examples=[1000], + default=0, + frozen=False, + repr=False, + ) + + # The dendrite Terminal Information. + dendrite: Optional[TerminalInfo] = Field( + title="dendrite", + description="Dendrite Terminal Information", + examples=["TerminalInfo"], + default=TerminalInfo(), + frozen=False, + repr=False, + ) + + # A axon terminal information + axon: Optional[TerminalInfo] = Field( + title="axon", + description="Axon Terminal Information", + examples=["TerminalInfo"], + default=TerminalInfo(), + frozen=False, + repr=False, + ) + + computed_body_hash: Optional[str] = Field( + title="computed_body_hash", + description="The computed body hash of the request.", + examples=["0x0813029319030129u4120u10841824y0182u091u230912u"], + default="", + frozen=True, + repr=False, + ) + + required_hash_fields: ClassVar[tuple[str, ...]] = () + + _extract_total_size = field_validator("total_size", mode="before")(cast_int) + + _extract_header_size = field_validator("header_size", mode="before")(cast_int) + + _extract_timeout = field_validator("timeout", mode="before")(cast_float) + + def __setattr__(self, name: str, value: Any): + """ + Override the :func:`__setattr__` method to make the ``required_hash_fields`` property read-only. + + This is a security mechanism such that the ``required_hash_fields`` property cannot be + overridden by the user or malicious code. + """ + if name == "body_hash": + raise AttributeError( + "body_hash property is read-only and cannot be overridden." + ) + super().__setattr__(name, value) + + def get_total_size(self) -> int: + """ + Get the total size of the current object. + + This method first calculates the size of the current object, then assigns it + to the instance variable :func:`self.total_size` and finally returns this value. + + Returns: + int: The total size of the current object. + """ + self.total_size = get_size(self) + return self.total_size + + @property + def is_success(self) -> bool: + """ + Checks if the dendrite's status code indicates success. + + This method returns ``True`` if the status code of the dendrite is ``200``, + which typically represents a successful HTTP request. + + Returns: + bool: ``True`` if dendrite's status code is ``200``, ``False`` otherwise. + """ + return self.dendrite is not None and self.dendrite.status_code == 200 + + @property + def is_failure(self) -> bool: + """ + Checks if the dendrite's status code indicates failure. + + This method returns ``True`` if the status code of the dendrite is not ``200``, + which would mean the HTTP request was not successful. + + Returns: + bool: ``True`` if dendrite's status code is not ``200``, ``False`` otherwise. + """ + return self.dendrite is not None and self.dendrite.status_code != 200 + + @property + def is_timeout(self) -> bool: + """ + Checks if the dendrite's status code indicates a timeout. + + This method returns ``True`` if the status code of the dendrite is ``408``, + which is the HTTP status code for a request timeout. + + Returns: + bool: ``True`` if dendrite's status code is ``408``, ``False`` otherwise. + """ + return self.dendrite is not None and self.dendrite.status_code == 408 + + @property + def is_blacklist(self) -> bool: + """ + Checks if the dendrite's status code indicates a blacklisted request. + + This method returns ``True`` if the status code of the dendrite is ``403``, + which is the HTTP status code for a forbidden request. + + Returns: + bool: ``True`` if dendrite's status code is ``403``, ``False`` otherwise. + """ + return self.dendrite is not None and self.dendrite.status_code == 403 + + @property + def failed_verification(self) -> bool: + """ + Checks if the dendrite's status code indicates failed verification. + + This method returns ``True`` if the status code of the dendrite is ``401``, + which is the HTTP status code for unauthorized access. + + Returns: + bool: ``True`` if dendrite's status code is ``401``, ``False`` otherwise. + """ + return self.dendrite is not None and self.dendrite.status_code == 401 + + def get_required_fields(self): + """ + Get the required fields from the model's JSON schema. + """ + schema = self.__class__.model_json_schema() + return schema.get("required", []) + + def to_headers(self) -> dict: + """ + Converts the state of a Synapse instance into a dictionary of HTTP headers. + + This method is essential for + packaging Synapse data for network transmission in the Bittensor framework, ensuring that each key aspect of + the Synapse is represented in a format suitable for HTTP communication. + + Process: + + 1. Basic Information: It starts by including the ``name`` and ``timeout`` of the Synapse, which are fundamental + for identifying the query and managing its lifespan on the network. + 2. Complex Objects: The method serializes the ``axon`` and ``dendrite`` objects, if present, into strings. This + serialization is crucial for preserving the state and structure of these objects over the network. + 3. Encoding: Non-optional complex objects are serialized and encoded in base64, making them safe for HTTP transport. + 4. Size Metrics: The method calculates and adds the size of headers and the total object size, providing + valuable information for network bandwidth management. + + Example Usage:: + + synapse = Synapse(name="ExampleSynapse", timeout=30) + headers = synapse.to_headers() + # headers now contains a dictionary representing the Synapse instance + + Returns: + dict: A dictionary containing key-value pairs representing the Synapse's properties, suitable for HTTP + communication. + """ + # Initializing headers with 'name' and 'timeout' + headers = {"name": self.name, "timeout": str(self.timeout)} + + # Adding headers for 'axon' and 'dendrite' if they are not None + if self.axon: + headers.update( + { + f"bt_header_axon_{k}": str(v) + for k, v in self.axon.model_dump().items() + if v is not None + } + ) + if self.dendrite: + headers.update( + { + f"bt_header_dendrite_{k}": str(v) + for k, v in self.dendrite.model_dump().items() + if v is not None + } + ) + + # Getting the fields of the instance + instance_fields = self.model_dump() + + # 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 + + elif required and field in required: + try: + # create an empty (dummy) instance of type(value) to pass pydantic validation on the axon side + serialized_value = json.dumps(value.__class__.__call__()) + encoded_value = base64.b64encode(serialized_value.encode()).decode( + "utf-8" + ) + headers[f"bt_header_input_obj_{field}"] = encoded_value + except TypeError as e: + raise ValueError( + f"Error serializing {field} with value {value}. Objects must be json serializable." + ) from e + + # Adding the size of the headers and the total size to the headers + headers["header_size"] = str(sys.getsizeof(headers)) + headers["total_size"] = str(self.get_total_size()) + headers["computed_body_hash"] = self.body_hash + + return headers + + @property + def body_hash(self) -> str: + """ + Computes a SHA3-256 hash of the serialized body of the Synapse instance. + + This hash is used to + ensure the data integrity and security of the Synapse instance when it's transmitted across the + network. It is a crucial feature for verifying that the data received is the same as the data sent. + + Process: + + 1. Iterates over each required field as specified in ``required_hash_fields``. + 2. Concatenates the string representation of these fields. + 3. Applies SHA3-256 hashing to the concatenated string to produce a unique fingerprint of the data. + + Example:: + + synapse = Synapse(name="ExampleRoute", timeout=10) + hash_value = synapse.body_hash + # hash_value is the SHA3-256 hash of the serialized body of the Synapse instance + + Returns: + str: The SHA3-256 hash as a hexadecimal string, providing a fingerprint of the Synapse instance's data for + integrity checks. + """ + hashes = [] + + hash_fields_field = self.model_fields.get("required_hash_fields") + instance_fields = None + if hash_fields_field: + warnings.warn( + "The 'required_hash_fields' field handling deprecated and will be removed. " + "Please update Synapse class definition to use 'required_hash_fields' class variable instead.", + DeprecationWarning, + ) + required_hash_fields = hash_fields_field.default + + if required_hash_fields: + instance_fields = self.model_dump() + # Preserve backward compatibility in which fields will added in .model_dump() order + # instead of the order one from `self.required_hash_fields` + required_hash_fields = [ + field for field in instance_fields if field in required_hash_fields + ] + + # Hack to cache the required hash fields names + if len(required_hash_fields) == len(required_hash_fields): + self.__class__.required_hash_fields = tuple(required_hash_fields) + else: + required_hash_fields = self.__class__.required_hash_fields + + if required_hash_fields: + instance_fields = instance_fields or self.model_dump() + for field in required_hash_fields: + hashes.append(get_hash(str(instance_fields[field]))) + + return get_hash("".join(hashes)) + + @classmethod + def parse_headers_to_inputs(cls, headers: dict) -> dict: + """ + Interprets and transforms a given dictionary of headers into a structured dictionary, facilitating the + reconstruction of Synapse objects. + + This method is essential for parsing network-transmitted + data back into a Synapse instance, ensuring data consistency and integrity. + + Process: + + 1. Separates headers into categories based on prefixes (``axon``, ``dendrite``, etc.). + 2. Decodes and deserializes ``input_obj`` headers into their original objects. + 3. Assigns simple fields directly from the headers to the input dictionary. + + Example:: + + received_headers = { + 'bt_header_axon_address': '127.0.0.1', + 'bt_header_dendrite_port': '8080', + # Other headers... + } + inputs = Synapse.parse_headers_to_inputs(received_headers) + # inputs now contains a structured representation of Synapse properties based on the headers + + Note: + This is handled automatically when calling :func:`Synapse.from_headers(headers)` and does not need to be + called directly. + + Args: + headers (dict): The headers dictionary to parse. + + Returns: + dict: A structured dictionary representing the inputs for constructing a Synapse instance. + """ + + # Initialize the input dictionary with empty sub-dictionaries for 'axon' and 'dendrite' + inputs_dict: dict[str, Union[dict, Optional[str]]] = { + "axon": {}, + "dendrite": {}, + } + + # Iterate over each item in the headers + for key, value in headers.items(): + # Handle 'axon' headers + if "bt_header_axon_" in key: + try: + new_key = key.split("bt_header_axon_")[1] + axon_dict = cast(dict, inputs_dict["axon"]) + axon_dict[new_key] = value + except Exception as e: + logging.error(f"Error while parsing 'axon' header {key}: {str(e)}") + continue + # Handle 'dendrite' headers + elif "bt_header_dendrite_" in key: + try: + new_key = key.split("bt_header_dendrite_")[1] + dendrite_dict = cast(dict, inputs_dict["dendrite"]) + dendrite_dict[new_key] = value + except Exception as e: + logging.error(f"Error while parsing 'dendrite' header {key}: {e}") + continue + # Handle 'input_obj' headers + elif "bt_header_input_obj" in key: + try: + new_key = key.split("bt_header_input_obj_")[1] + # Skip if the key already exists in the dictionary + if new_key in inputs_dict: + continue + # Decode and load the serialized object + inputs_dict[new_key] = json.loads( + base64.b64decode(value.encode()).decode("utf-8") + ) + except json.JSONDecodeError as e: + logging.error( + f"Error while json decoding 'input_obj' header {key}: {e}" + ) + continue + except Exception as e: + logging.error(f"Error while parsing 'input_obj' header {key}: {e}") + continue + else: + # setting this to warning fills up logs unnecessarily + logging.trace(f"Unexpected header key encountered: {key}") + + # Assign the remaining known headers directly + inputs_dict["timeout"] = headers.get("timeout", None) + inputs_dict["name"] = headers.get("name", None) + inputs_dict["header_size"] = headers.get("header_size", None) + inputs_dict["total_size"] = headers.get("total_size", None) + inputs_dict["computed_body_hash"] = headers.get("computed_body_hash", None) + + return inputs_dict + + @classmethod + def from_headers(cls, headers: dict) -> "Synapse": + """ + Constructs a new Synapse instance from a given headers dictionary, enabling the re-creation of the Synapse's + state as it was prior to network transmission. + + This method is a key part of the + deserialization process in the Bittensor network, allowing nodes to accurately reconstruct Synapse + objects from received data. + + Example:: + + received_headers = { + 'bt_header_axon_address': '127.0.0.1', + 'bt_header_dendrite_port': '8080', + # Other headers... + } + synapse = Synapse.from_headers(received_headers) + # synapse is a new Synapse instance reconstructed from the received headers + + Args: + headers (dict): The dictionary of headers containing serialized Synapse information. + + Returns: + bittensor.core.synapse.Synapse: A new instance of Synapse, reconstructed from the parsed header information, + replicating the original instance's state. + """ + + # Get the inputs dictionary from the headers + input_dict = cls.parse_headers_to_inputs(headers) + + # Use the dictionary unpacking operator to pass the inputs to the class constructor + synapse = cls(**input_dict) + + return synapse diff --git a/bittensor/core/tensor.py b/bittensor/core/tensor.py new file mode 100644 index 0000000000..5cafd0986a --- /dev/null +++ b/bittensor/core/tensor.py @@ -0,0 +1,232 @@ +import base64 +from typing import Optional, Union + +import msgpack +import msgpack_numpy +import numpy as np +from pydantic import ConfigDict, BaseModel, Field, field_validator + +from bittensor.utils.registration import torch, use_torch + + +class DTypes(dict): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.torch: bool = False + self.update( + { + "float16": np.float16, + "float32": np.float32, + "float64": np.float64, + "uint8": np.uint8, + "int16": np.int16, + "int8": np.int8, + "int32": np.int32, + "int64": np.int64, + "bool": bool, + } + ) + + def __getitem__(self, key): + self._add_torch() + return super().__getitem__(key) + + def __contains__(self, key): + self._add_torch() + return super().__contains__(key) + + def _add_torch(self): + if self.torch is False: + torch_dtypes = { + "torch.float16": torch.float16, + "torch.float32": torch.float32, + "torch.float64": torch.float64, + "torch.uint8": torch.uint8, + "torch.int16": torch.int16, + "torch.int8": torch.int8, + "torch.int32": torch.int32, + "torch.int64": torch.int64, + "torch.bool": torch.bool, + } + self.update(torch_dtypes) + self.torch = True + + +dtypes = DTypes() + + +def cast_dtype(raw: Union[None, np.dtype, "torch.dtype", str]) -> Optional[str]: + """ + Casts the raw value to a string representing the `numpy data type `_, or the `torch data type `_ if using torch. + + Args: + raw (Union[None, numpy.dtype, torch.dtype, str]): The raw value to cast. + + Returns: + str: The string representing the numpy/torch data type. + + Raises: + Exception: If the raw value is of an invalid type. + """ + if not raw: + return None + if use_torch() and isinstance(raw, torch.dtype): + return dtypes[raw] + elif isinstance(raw, np.dtype): + return dtypes[raw] + elif isinstance(raw, str): + if use_torch(): + assert raw in dtypes, f"{raw} not a valid torch type in dict {dtypes}" + return raw + else: + assert raw in dtypes, f"{raw} not a valid numpy type in dict {dtypes}" + return raw + else: + raise Exception( + f"{raw} of type {type(raw)} does not have a valid type in Union[None, numpy.dtype, torch.dtype, str]" + ) + + +def cast_shape(raw: Union[None, list[int], str]) -> Optional[Union[str, list]]: + """ + Casts the raw value to a string representing the tensor shape. + + Args: + raw (Union[None, list[int], str]): The raw value to cast. + + Returns: + str: The string representing the tensor shape. + + Raises: + Exception: If the raw value is of an invalid type or if the list elements are not of type int. + """ + if not raw: + return None + elif isinstance(raw, list): + if len(raw) == 0 or isinstance(raw[0], int): + return raw + else: + raise Exception(f"{raw} list elements are not of type int") + elif isinstance(raw, str): + shape = list(map(int, raw.split("[")[1].split("]")[0].split(","))) + return shape + else: + raise Exception( + f"{raw} of type {type(raw)} does not have a valid type in Union[None, list[int], str]" + ) + + +class tensor: + def __new__(cls, tensor: Union[list, "np.ndarray", "torch.Tensor"]): + if isinstance(tensor, list) or isinstance(tensor, np.ndarray): + tensor = torch.tensor(tensor) if use_torch() else np.array(tensor) + return Tensor.serialize(tensor_=tensor) + + +class Tensor(BaseModel): + """ + Represents a Tensor object. + + Args: + buffer (Optional[str]): Tensor buffer data. + dtype (str): Tensor data type. + shape (list[int]): Tensor shape. + """ + + model_config = ConfigDict(validate_assignment=True) + + def tensor(self) -> Union[np.ndarray, "torch.Tensor"]: + return self.deserialize() + + def tolist(self) -> list[object]: + return self.deserialize().tolist() + + def numpy(self) -> "np.ndarray": + return ( + self.deserialize().detach().numpy() if use_torch() else self.deserialize() + ) + + def deserialize(self) -> Union["np.ndarray", "torch.Tensor"]: + """ + Deserializes the Tensor object. + + Returns: + np.array or torch.Tensor: The deserialized tensor object. + + Raises: + Exception: If the deserialization process encounters an error. + """ + shape = tuple(self.shape) + buffer_bytes = base64.b64decode(self.buffer.encode("utf-8")) + numpy_object = msgpack.unpackb( + buffer_bytes, object_hook=msgpack_numpy.decode + ).copy() + if use_torch(): + torch_object = torch.as_tensor(numpy_object) + # Reshape does not work for (0) or [0] + if not (len(shape) == 1 and shape[0] == 0): + torch_object = torch_object.reshape(shape) + return torch_object.type(dtypes[self.dtype]) + else: + # Reshape does not work for (0) or [0] + if not (len(shape) == 1 and shape[0] == 0): + numpy_object = numpy_object.reshape(shape) + return numpy_object.astype(dtypes[self.dtype]) + + @staticmethod + def serialize(tensor_: Union["np.ndarray", "torch.Tensor"]) -> "Tensor": + """ + Serializes the given tensor. + + Args: + tensor_ (np.array or torch.Tensor): The tensor to serialize. + + Returns: + :func:`Tensor`: The serialized tensor. + + Raises: + Exception: If the serialization process encounters an error. + """ + dtype = str(tensor_.dtype) + shape = list(tensor_.shape) + if len(shape) == 0: + shape = [0] + tensor__ = tensor_.cpu().detach().numpy().copy() if use_torch() else tensor_ + data_buffer = base64.b64encode( + msgpack.packb(tensor__, default=msgpack_numpy.encode) + ).decode("utf-8") + return Tensor(buffer=data_buffer, shape=shape, dtype=dtype) + + # Represents the tensor buffer data. + buffer: Optional[str] = Field( + default=None, + title="buffer", + description="Tensor buffer data. This field stores the serialized representation of the tensor data.", + examples=["0x321e13edqwds231231231232131"], + frozen=True, + repr=False, + ) + + # Represents the data type of the tensor. + dtype: str = Field( + title="dtype", + description="Tensor data type. This field specifies the data type of the tensor, such as numpy.float32 or torch.int64.", + examples=["np.float32"], + frozen=True, + repr=True, + ) + + # Represents the shape of the tensor. + shape: list[int] = Field( + title="shape", + description="Tensor shape. This field defines the dimensions of the tensor as a list of integers, such as [10, 10] for a 2D tensor with shape (10, 10).", + examples=[10, 10], + frozen=True, + repr=True, + ) + + # Extract the represented shape of the tensor. + _extract_shape = field_validator("shape", mode="before")(cast_shape) + + # Extract the represented data type of the tensor. + _extract_dtype = field_validator("dtype", mode="before")(cast_dtype) diff --git a/bittensor/utils/ptp.py b/bittensor/core/threadpool.py similarity index 51% rename from bittensor/utils/ptp.py rename to bittensor/core/threadpool.py index 3763aa7c2c..bca3ad014c 100644 --- a/bittensor/utils/ptp.py +++ b/bittensor/core/threadpool.py @@ -1,20 +1,26 @@ # Copyright 2009 Brian Quinlan. All Rights Reserved. # Licensed to PSF under a Contributor Agreement. -"""Implements ThreadPoolExecutor.""" +"""Implements `ThreadPoolExecutor `_.""" -__author__ = 'Brian Quinlan (brian@sweetapp.com)' +__author__ = "Brian Quinlan (brian@sweetapp.com)" -import atexit -from concurrent.futures import _base -from loguru import logger +import argparse import itertools +import logging +import os import queue import random +import sys import threading +import time import weakref -import os -import sys +from concurrent.futures import _base +from typing import Callable + +from bittensor.core.config import Config +from bittensor.core.settings import BLOCKTIME +from bittensor.utils.btlogging.defines import BITTENSOR_LOGGER_NAME # Workers are created as daemon threads. This is done to allow the interpreter # to exit when there are still idle threads in a ThreadPoolExecutor's thread @@ -30,32 +36,26 @@ # workers to exit when their work queues are empty and then waits until the # threads finish. +logger = logging.getLogger(BITTENSOR_LOGGER_NAME) + _threads_queues = weakref.WeakKeyDictionary() _shutdown = False -def _python_exit(): - global _shutdown - _shutdown = True - items = list(_threads_queues.items()) - for t, q in items: - q.put(NULL_ENTRY) - for t, q in items: - t.join() - if t.is_alive(): - logger.error('thread was not joined {}', t) - -atexit.register(_python_exit) - class _WorkItem(object): - def __init__(self, future, fn, args, kwargs): + def __init__(self, future, fn, start_time, args, kwargs): self.future = future self.fn = fn + self.start_time = start_time self.args = args self.kwargs = kwargs def run(self): - if not self.future.set_running_or_notify_cancel(): + """Run the given work item""" + # Checks if future is canceled or if work item is stale + if (not self.future.set_running_or_notify_cancel()) or ( + time.time() - self.start_time > BLOCKTIME + ): return try: @@ -68,14 +68,15 @@ def run(self): self.future.set_result(result) -NULL_ENTRY = (-sys.maxsize, _WorkItem(None, None, (), {})) +NULL_ENTRY = (sys.maxsize, _WorkItem(None, None, time.time(), (), {})) + def _worker(executor_reference, work_queue, initializer, initargs): if initializer is not None: try: initializer(*initargs) except BaseException: - _base.LOGGER.critical('Exception in initializer:', exc_info=True) + _base.LOGGER.critical("Exception in initializer:", exc_info=True) executor = executor_reference() if executor is not None: executor._initializer_failed() @@ -85,14 +86,14 @@ def _worker(executor_reference, work_queue, initializer, initargs): work_item = work_queue.get(block=True) priority = work_item[0] item = work_item[1] - if priority == -sys.maxsize: + if priority == sys.maxsize: del item elif item is not None: item.run() # Delete references to object. See issue16284 del item continue - + executor = executor_reference() # Exit if: # - The interpreter is shutting down OR @@ -108,24 +109,31 @@ def _worker(executor_reference, work_queue, initializer, initargs): return del executor except BaseException: - logger.error('work_item', work_item) - _base.LOGGER.critical('Exception in worker', exc_info=True) + logger.error("work_item", work_item) + _base.LOGGER.critical("Exception in worker", exc_info=True) class BrokenThreadPool(_base.BrokenExecutor): """ - Raised when a worker thread in a ThreadPoolExecutor failed initializing. + Raised when a worker thread in a `ThreadPoolExecutor `_ failed initializing. """ -class ThreadPoolExecutor(_base.Executor): +class PriorityThreadPoolExecutor(_base.Executor): + """Base threadpool executor with a priority queue.""" # Used to assign unique thread names when thread_name_prefix is not supplied. _counter = itertools.count().__next__ - def __init__(self, maxsize = -1, max_workers=None, thread_name_prefix='', - initializer=None, initargs=()): - """Initializes a new ThreadPoolExecutor instance. + def __init__( + self, + maxsize=-1, + max_workers=None, + thread_name_prefix="", + initializer=None, + initargs=(), + ): + """Initializes a new `ThreadPoolExecutor `_ instance. Args: max_workers: The maximum number of threads that can be used to @@ -145,56 +153,114 @@ def __init__(self, maxsize = -1, max_workers=None, thread_name_prefix='', raise TypeError("initializer must be a callable") self._max_workers = max_workers - self._work_queue = queue.PriorityQueue(maxsize = maxsize) + self._work_queue = queue.PriorityQueue(maxsize=maxsize) + self._idle_semaphore = threading.Semaphore(0) self._threads = set() self._broken = False self._shutdown = False self._shutdown_lock = threading.Lock() - self._thread_name_prefix = (thread_name_prefix or - ("ThreadPoolExecutor-%d" % self._counter())) + self._thread_name_prefix = thread_name_prefix or ( + "ThreadPoolExecutor-%d" % self._counter() + ) self._initializer = initializer self._initargs = initargs - def submit(self, fn, *args, **kwargs): + @classmethod + def add_args(cls, parser: argparse.ArgumentParser, prefix: str = None): + """Accept specific arguments from parser""" + prefix_str = "" if prefix is None else prefix + "." + try: + default_max_workers = ( + os.getenv("BT_PRIORITY_MAX_WORKERS") + if os.getenv("BT_PRIORITY_MAX_WORKERS") is not None + else 5 + ) + default_maxsize = ( + os.getenv("BT_PRIORITY_MAXSIZE") + if os.getenv("BT_PRIORITY_MAXSIZE") is not None + else 10 + ) + parser.add_argument( + "--" + prefix_str + "priority.max_workers", + type=int, + help="""maximum number of threads in thread pool""", + default=default_max_workers, + ) + parser.add_argument( + "--" + prefix_str + "priority.maxsize", + type=int, + help="""maximum size of tasks in priority queue""", + default=default_maxsize, + ) + except argparse.ArgumentError: + # re-parsing arguments. + pass + + @classmethod + def config(cls) -> "Config": + """Get config from the argument parser. + + Return: :func:`bittensor.Config` object. + """ + parser = argparse.ArgumentParser() + PriorityThreadPoolExecutor.add_args(parser) + return Config(parser) + + @property + def is_empty(self): + return self._work_queue.empty() + + def submit(self, fn: Callable, *args, **kwargs) -> _base.Future: with self._shutdown_lock: if self._broken: raise BrokenThreadPool(self._broken) if self._shutdown: - raise RuntimeError('cannot schedule new futures after shutdown') + raise RuntimeError("cannot schedule new futures after shutdown") if _shutdown: - raise RuntimeError('cannot schedule new futures after ' - 'interpreter shutdown') - - priority = kwargs.get('priority', random.randint(0, sys.maxsize-1)) - if 'priority' in kwargs: - del kwargs['priority'] + raise RuntimeError( + "cannot schedule new futures after interpreter shutdown" + ) + + priority = kwargs.get("priority", random.randint(0, 1000000)) + if priority == 0: + priority = random.randint(1, 100) + epsilon = random.uniform(0, 0.01) * priority + start_time = time.time() + if "priority" in kwargs: + del kwargs["priority"] f = _base.Future() - w = _WorkItem(f, fn, args, kwargs) - - self._work_queue.put((priority, w), block=False) + w = _WorkItem(f, fn, start_time, args, kwargs) + self._work_queue.put((-float(priority + epsilon), w), block=False) self._adjust_thread_count() return f - submit.__doc__ = _base.Executor.submit.__doc__ + submit.__doc__ = _base.Executor.submit.__doc__ def _adjust_thread_count(self): + # if idle threads are available, don't spin new threads + if self._idle_semaphore.acquire(timeout=0): + return + # When the executor gets lost, the weakref callback will wake up # the worker threads. def weakref_cb(_, q=self._work_queue): q.put(NULL_ENTRY) - # TODO(bquinlan): Should avoid creating new threads if there are more - # idle threads than items in the work queue. + num_threads = len(self._threads) if num_threads < self._max_workers: - thread_name = '%s_%d' % (self._thread_name_prefix or self, - num_threads) - t = threading.Thread(name=thread_name, target=_worker, - args=(weakref.ref(self, weakref_cb), - self._work_queue, - self._initializer, - self._initargs)) + thread_name = "%s_%d" % (self._thread_name_prefix or self, num_threads) + t = threading.Thread( + name=thread_name, + target=_worker, + args=( + weakref.ref(self, weakref_cb), + self._work_queue, + self._initializer, + self._initargs, + ), + ) t.daemon = True t.start() self._threads.add(t) @@ -202,8 +268,9 @@ def weakref_cb(_, q=self._work_queue): def _initializer_failed(self): with self._shutdown_lock: - self._broken = ('A thread initializer failed, the thread pool ' - 'is not usable anymore') + self._broken = ( + "A thread initializer failed, the thread pool is not usable anymore" + ) # Drain work queue and mark pending futures failed while True: try: @@ -214,14 +281,15 @@ def _initializer_failed(self): work_item.future.set_exception(BrokenThreadPool(self._broken)) def shutdown(self, wait=True): - pass - - # with self._shutdown_lock: - # self._shutdown = True - # self._work_queue.put(NULL_ENTRY) - - # logger.info('wait') - # if wait: - # for t in self._threads: - # t.join(timeout=1) - shutdown.__doc__ = _base.Executor.shutdown.__doc__ \ No newline at end of file + with self._shutdown_lock: + self._shutdown = True + self._work_queue.put(NULL_ENTRY) + + if wait: + for t in self._threads: + try: + t.join(timeout=2) + except Exception: + pass + + shutdown.__doc__ = _base.Executor.shutdown.__doc__ diff --git a/bittensor/core/timelock.py b/bittensor/core/timelock.py new file mode 100644 index 0000000000..ed0b7c892e --- /dev/null +++ b/bittensor/core/timelock.py @@ -0,0 +1,184 @@ +""" +This module provides functionality for TimeLock Encryption (TLE), a mechanism that encrypts data such that it can +only be decrypted after a specific amount of time (expressed in the form of Drand rounds). It includes functions +for encryption, decryption, and handling the decryption process by waiting for the reveal round. The logic is based on +Drand QuickNet. + +Main Functions: + - encrypt: Encrypts data and returns the encrypted data along with the reveal round. + - decrypt: Decrypts the provided encrypted data when the reveal round is reached. + - wait_reveal_and_decrypt: Waits for the reveal round and decrypts the encrypted data. + +Usage Example: + ```python + from bittensor import timelock + data = "From Cortex to Bittensor" + encrypted_data, reveal_round = timelock.encrypt(data, n_blocks=5) + decrypted_data = timelock.wait_reveal_and_decrypt(encrypted_data) + ``` + +Usage Example with custom data: + ```python + import pickle + from dataclasses import dataclass + + from bittensor import timelock + + + @dataclass + class Person: + name: str + age: int + + # get instance of your data + x_person = Person("X Lynch", 123) + + # get bytes of your instance + byte_data = pickle.dumps(x_person) + + # get TLE encoded bytes + encrypted, reveal_round = timelock.encrypt(byte_data, 1) + + # wait when reveal round appears in Drand QuickNet and get decrypted data + decrypted = timelock.wait_reveal_and_decrypt(encrypted_data=encrypted) + + # convert bytes into your instance back + x_person_2 = pickle.loads(decrypted) + + # make sure initial and decoded instances are the same + assert x_person == x_person_2 + ``` + +Note: +For handling fast-block nodes, set the `block_time` parameter to 0.25 seconds during encryption. +""" + +import struct +import time +from typing import Optional, Union + +from bittensor_drand import ( + encrypt as _btr_encrypt, + decrypt as _btr_decrypt, + get_latest_round, +) + +TLE_ENCRYPTED_DATA_SUFFIX = b"AES_GCM_" + + +def encrypt( + data: Union[bytes, str], n_blocks: int, block_time: Union[int, float] = 12.0 +) -> tuple[bytes, int]: + """Encrypts data using TimeLock Encryption + + Arguments: + data: Any bytes data to be encrypted. + n_blocks: Number of blocks to encrypt. + block_time: Time in seconds for each block. Default is `12.0` seconds. + + Returns: + tuple: A tuple containing the encrypted data and reveal TimeLock reveal round. + + Raises: + PyValueError: If failed to encrypt data. + + Usage: + data = "From Cortex to Bittensor" + + # default usage + encrypted_data, reveal_round = encrypt(data, 10) + + # passing block_time for fast-blocks node + encrypted_data, reveal_round = encrypt(data, 15, block_time=0.25) + + encrypted_data, reveal_round = encrypt(data, 5) + + + Note: + For using this function with fast-blocks node you need to set block_time to 0.25 seconds. + data, round = encrypt(data, n_blocks, block_time=0.25) + """ + if isinstance(data, str): + data = data.encode() + return _btr_encrypt(data, n_blocks, block_time) + + +def decrypt( + encrypted_data: bytes, no_errors: bool = True, return_str: bool = False +) -> Optional[Union[bytes, str]]: + """Decrypts encrypted data using TimeLock Decryption + + Arguments: + encrypted_data: Encrypted data to be decrypted. + no_errors: If True, no errors will be raised during decryption. + return_str: convert decrypted data to string if `True`. Default is `False`. + + Returns: + decrypted_data: Decrypted data, when reveled round is reached. + + Usage: + # default usage + decrypted_data = decrypt(encrypted_data) + + # passing no_errors=False for raising errors during decryption + decrypted_data = decrypt(encrypted_data, no_errors=False) + + # passing return_str=True for returning decrypted data as string + decrypted_data = decrypt(encrypted_data, return_str=True) + """ + result = _btr_decrypt(encrypted_data, no_errors) + if result is None: + return None + if return_str: + return result.decode() + return result + + +def wait_reveal_and_decrypt( + encrypted_data: bytes, + reveal_round: Optional[int] = None, + no_errors: bool = True, + return_str: bool = False, +) -> bytes: + """ + Waits for reveal round and decrypts data using TimeLock Decryption. + + Arguments: + encrypted_data: Encrypted data to be decrypted. + reveal_round: Reveal round to wait for. If None, will be parsed from encrypted data. + no_errors: If True, no errors will be raised during decryption. + return_str: convert decrypted data to string if `True`. Default is `False`. + + Raises: + struct.error: If failed to parse reveal round from encrypted data. + TypeError: If reveal_round is None or wrong type. + IndexError: If provided encrypted_data does not contain reveal round. + + Returns: + bytes: Decrypted data. + + Usage: + import bittensor as bt + encrypted, reveal_round = bt.timelock.encrypt("Cortex is power", 3) + """ + if reveal_round is None: + try: + reveal_round = struct.unpack( + " "Config": + """ + Creates and returns a Bittensor configuration object. + + Returns: + config (bittensor.core.config.Config): A Bittensor configuration object configured with arguments added by + the `subtensor.add_args` method. + """ + parser = argparse.ArgumentParser() + SubtensorMixin.add_args(parser) + return Config(parser) + + @staticmethod + def setup_config(network: Optional[str], config: "Config"): + """ + Sets up and returns the configuration for the Subtensor network and endpoint. + + This method determines the appropriate network and chain endpoint based on the provided network string or + configuration object. It evaluates the network and endpoint in the following order of precedence: + 1. Provided network string. + 2. Configured chain endpoint in the `config` object. + 3. Configured network in the `config` object. + 4. Default chain endpoint. + 5. Default network. + + Arguments: + network (Optional[str]): The name of the Subtensor network. If None, the network and endpoint will be + determined from the `config` object. + config (bittensor.core.config.Config): The configuration object containing the network and chain endpoint + settings. + + Returns: + tuple: A tuple containing the formatted WebSocket endpoint URL and the evaluated network name. + """ + if network is None: + candidates = [ + ( + config.is_set("subtensor.chain_endpoint"), + config.subtensor.chain_endpoint, + ), + (config.is_set("subtensor.network"), config.subtensor.network), + ( + config.subtensor.get("chain_endpoint"), + config.subtensor.chain_endpoint, + ), + (config.subtensor.get("network"), config.subtensor.network), + ] + for check, config_network in candidates: + if check: + network = config_network + + evaluated_network, evaluated_endpoint = determine_chain_endpoint_and_network( + network + ) + + return networking.get_formatted_ws_endpoint_url( + evaluated_endpoint + ), evaluated_network + + @classmethod + def help(cls): + """Print help to stdout.""" + parser = argparse.ArgumentParser() + cls.add_args(parser) + print(cls.__new__.__doc__) + parser.print_help() + + @classmethod + def add_args(cls, parser: "argparse.ArgumentParser", prefix: Optional[str] = None): + """ + Adds command-line arguments to the provided ArgumentParser for configuring the Subtensor settings. + + Arguments: + parser (argparse.ArgumentParser): The ArgumentParser object to which the Subtensor arguments will be added. + prefix (Optional[str]): An optional prefix for the argument names. If provided, the prefix is prepended to + each argument name. + + Arguments added: + --subtensor.network: The Subtensor network flag. Possible values are 'finney', 'test', 'archive', and + 'local'. Overrides the chain endpoint if set. + --subtensor.chain_endpoint: The Subtensor chain endpoint flag. If set, it overrides the network flag. + --subtensor._mock: If true, uses a mocked connection to the chain. + + Example: + parser = argparse.ArgumentParser() + Subtensor.add_args(parser) + """ + prefix_str = "" if prefix is None else f"{prefix}." + try: + default_network = settings.DEFAULTS.subtensor.network + default_chain_endpoint = settings.DEFAULTS.subtensor.chain_endpoint + + parser.add_argument( + f"--{prefix_str}subtensor.network", + default=default_network, + type=str, + help="""The subtensor network flag. The likely choices are: + -- finney (main network) + -- test (test network) + -- archive (archive network +300 blocks) + -- local (local running network) + If this option is set it overloads subtensor.chain_endpoint with + an entry point node from that network. + """, + ) + parser.add_argument( + f"--{prefix_str}subtensor.chain_endpoint", + default=default_chain_endpoint, + type=str, + help="""The subtensor endpoint flag. If set, overrides the --network flag.""", + ) + parser.add_argument( + f"--{prefix_str}subtensor._mock", + default=False, + type=bool, + help="""If true, uses a mocked connection to the chain.""", + ) + + except argparse.ArgumentError: + # re-parsing arguments. + pass + + +class AxonServeCallParams: + def __init__( + self, + version: int, + ip: int, + port: int, + ip_type: int, + netuid: int, + hotkey: str, + coldkey: str, + protocol: int, + placeholder1: int, + placeholder2: int, + certificate: Optional[Certificate], + ): + self.version = version + self.ip = ip + self.port = port + self.ip_type = ip_type + self.netuid = netuid + self.hotkey = hotkey + self.coldkey = coldkey + self.protocol = protocol + self.placeholder1 = placeholder1 + self.placeholder2 = placeholder2 + self.certificate = certificate + + def __eq__(self, other): + if isinstance(other, self.__class__): + return all( + getattr(self, attr) == getattr(other, attr) for attr in self.__dict__ + ) + elif isinstance(other, dict): + return all(getattr(self, attr) == other.get(attr) for attr in self.__dict__) + elif isinstance(other, (NeuronInfo, NeuronInfoLite)): + return all( + [ + self.version == other.axon_info.version, + self.ip == networking.ip_to_int(other.axon_info.ip), + self.port == other.axon_info.port, + self.ip_type == other.axon_info.ip_type, + self.netuid == other.netuid, + self.hotkey == other.hotkey, + self.coldkey == other.coldkey, + self.protocol == other.axon_info.protocol, + self.placeholder1 == other.axon_info.placeholder1, + self.placeholder2 == other.axon_info.placeholder2, + ] + ) + else: + raise NotImplementedError( + f"AxonServeCallParams equality not implemented for {type(other)}" + ) + + def copy(self) -> "AxonServeCallParams": + return self.__class__( + self.version, + self.ip, + self.port, + self.ip_type, + self.netuid, + self.hotkey, + self.coldkey, + self.protocol, + self.placeholder1, + self.placeholder2, + self.certificate, + ) + + def dict(self) -> dict: + """ + Returns a dict representation of this object. If `self.certificate` is `None`, + it is not included in this. + """ + d = { + "version": self.version, + "ip": self.ip, + "port": self.port, + "ip_type": self.ip_type, + "netuid": self.netuid, + "hotkey": self.hotkey, + "coldkey": self.coldkey, + "protocol": self.protocol, + "placeholder1": self.placeholder1, + "placeholder2": self.placeholder2, + } + if self.certificate is not None: + d["certificate"] = self.certificate + return d + + +class PrometheusServeCallParams(TypedDict): + """Prometheus serve chain call parameters.""" + + version: int + ip: int + port: int + ip_type: int + netuid: int + + +class ParamWithTypes(TypedDict): + name: str # Name of the parameter. + type: str # ScaleType string of the parameter. diff --git a/bittensor/crypto/__init__.py b/bittensor/crypto/__init__.py deleted file mode 100644 index f1f77fc148..0000000000 --- a/bittensor/crypto/__init__.py +++ /dev/null @@ -1,43 +0,0 @@ -from cryptography.exceptions import InvalidSignature, InvalidKey -from cryptography.fernet import Fernet, InvalidToken -from cryptography.hazmat.primitives import hashes -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC -from loguru import logger -import base64 - -class KeyError(Exception): - pass - -__SALT = b"Iguesscyborgslikemyselfhaveatendencytobeparanoidaboutourorigins" - -def encrypt(data, password): - - key = __generate_key(password) - - cipher_suite = Fernet(key) - return cipher_suite.encrypt(data) - -def decrypt_keypair(data, password): - - key = __generate_key(password) - - cipher_suite = Fernet(key) - return cipher_suite.decrypt(data) - -def __generate_key(password): - kdf = PBKDF2HMAC(algorithm=hashes.SHA256(), salt=__SALT, length=32, iterations=10000000, backend=default_backend()) - key = base64.urlsafe_b64encode(kdf.derive(password.encode())) - return key - - - -def is_encrypted(data): - return data[:6] == b"gAAAAA" - - -def decrypt_data(password, data): - try: - return decrypt_keypair(data, password) - except (InvalidSignature, InvalidKey, InvalidToken): - raise KeyError \ No newline at end of file diff --git a/bittensor/crypto/__pycache__/__init__.cpython-38.pyc b/bittensor/crypto/__pycache__/__init__.cpython-38.pyc deleted file mode 100644 index 1f41fe74b9..0000000000 Binary files a/bittensor/crypto/__pycache__/__init__.cpython-38.pyc and /dev/null differ diff --git a/bittensor/crypto/__pycache__/keyfiles.cpython-38.pyc b/bittensor/crypto/__pycache__/keyfiles.cpython-38.pyc deleted file mode 100644 index 1c7ab8c271..0000000000 Binary files a/bittensor/crypto/__pycache__/keyfiles.cpython-38.pyc and /dev/null differ diff --git a/bittensor/crypto/keyfiles.py b/bittensor/crypto/keyfiles.py deleted file mode 100644 index 540fbcc2b8..0000000000 --- a/bittensor/crypto/keyfiles.py +++ /dev/null @@ -1,17 +0,0 @@ -from bittensor.subtensor.interface import Keypair -import json -from loguru import logger - -class KeyFileError(Exception): - pass - -def load_keypair_from_data(data) -> Keypair: - try: - data = json.loads(data) - if "secretSeed" not in data: - raise KeyFileError("Keyfile corrupt") - - return Keypair.create_from_seed(data['secretSeed']) - except BaseException as e: - logger.debug(e) - raise KeyFileError("Keyfile corrupt") \ No newline at end of file diff --git a/bittensor/dendrite.py b/bittensor/dendrite.py deleted file mode 100644 index fddd4cd3ee..0000000000 --- a/bittensor/dendrite.py +++ /dev/null @@ -1,542 +0,0 @@ -import argparse -import asyncio -import grpc -import math -import sys -import time -import torch -import torch.nn as nn - -from torch.autograd.function import once_differentiable -from types import SimpleNamespace -from typing import Tuple, List, Optional -from loguru import logger -from munch import Munch - -import bittensor -from bittensor import bittensor_pb2_grpc as bittensor_grpc -from bittensor import bittensor_pb2 -from bittensor.serializer import PyTorchSerializer -from bittensor.exceptions.handlers import rollbar - -# dummy tensor that triggers autograd in a RemoteExpert -DUMMY = torch.empty(0, requires_grad=True) - -# Helper function for filling nill (zero) responses on failures. -def nill_response_for(inputs): - if torch.numel(inputs) == 0: - return torch.tensor([]) - return torch.zeros( (inputs.size(0), inputs.size(1), bittensor.__network_dim__), dtype = torch.float32) - -class Dendrite(nn.Module): - r""" - Bittensor object used to make calls to the network. It can be called like a normal torch nn.Module and is differentiable. - Messages passed through this module will be sent to neuron objects, either remote - or local, and return responses as torch tensors. Gradients passing through this module on a .backward() call will trigger - the Backward rpc calls, passing gradients to the remote neuron instances called during corresponding Forward operation. - - Args: - config (:obj:`bittensor.Config`, `required`): - Bittensor config object. - """ - - def __init__(self, config): - super().__init__() - self._config = config - self.__keypair = config.session.keypair - self._remotes = {} - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - parser.add_argument('--dendrite.pass_gradients', default=True, type=bool, - help='Switch to true is the neuron passes gradients to downstream peers.') - parser.add_argument('--dendrite.timeout', default=0.5, type=float, - help='Per request RPC timeout.') - parser.add_argument('--dendrite.do_backoff', default=True, type=bool, - help='Neurons who return non successful return codes are periodically not called with a multiplicative backoff.') - parser.add_argument('--dendrite.max_backoff', default=100, type=int, - help='Backoff saturates at this value.') - return parser - - @staticmethod - def check_config(config: Munch): - assert config.dendrite.timeout >= 0, 'timeout must be positive value, got {}'.format(config.dendrite.timeout) - - @property - def remotes(self): - return self._remotes.values() - - def forward_text(self, neurons: List[bittensor_pb2.Neuron], - x: List[torch.Tensor]) -> Tuple[List[torch.Tensor], torch.Tensor]: - r""" Forward text inputs to neurons. - - Args: - neurons (:obj:`List[bittensor_pb2.Neuron]` of shape :obj:`(num_neurons)`, `required`): - List of remote neurons which match length of x. Tensors from x are sent forward to these neurons. - - x (:obj:`List[torch.Tensor]` of shape :obj:`(num_neurons * [batch_size, sequence_len])`, `required`): - List of tensors to send to corresponsing neurons. Tensors are text input_ids encoded using the - bittensor tokenizer of shape [batch_size, sequence_len]. - - Returns: - forwad_output (:obj:`List[torch.FloatTensor]` of shape :obj:`(batch_size, sequence_len, bittensor.__network_dim__)`, `required`): - Output encodings of inputs produced by remote neurons. Non-responses are zeroes of common shape. - - return_codes (:obj:`List[torch.LongTensor]` of shape :obj:`[num_neurons]`, `required`): - dendrite call return ops. - """ - if len(x[0].shape) != 2: - error_msg = 'Text inputs should rank 2 with semantic shape: [batch_size, sequence_len]' - raise ValueError(error_msg) - if len(x) != len(neurons): - error_msg = 'List of text inputs x should have the same length as passed destination neurons, got {} and {}'.format(len(x), len(neurons)) - raise ValueError(error_msg) - if len(x) < 1: - error_msg = 'Must pass more than 0 input for argument x, got {}'.format(len(x)) - raise ValueError(error_msg) - return self.forward(neurons, x, bittensor_pb2.Modality.TEXT) - - def forward_image(self, neurons: List[bittensor_pb2.Neuron], - x: List[torch.Tensor]) -> Tuple[List[torch.Tensor], torch.Tensor]: - r""" Forward image inputs to neurons. - - Args: - neurons (:obj:`List[bittensor_pb2.Neuron]` of shape :obj:`(num_neurons)`, `required`): - List of remote neurons which match length of x. Tensors from x are sent forward to these neurons. - - x (:obj:`List[torch.Tensor]` of shape :obj:`(num_neurons * [batch_size, sequence_len, channels, rows, cols])`, `required`): - List of image-tensors to send to corresponsing neurons. Tensors are images encoded using the - torch.toTensor() or other encoding which produces the shape [batch_size, channels, rows, cols]. - - Returns: - forwad_output (:obj:`List[torch.FloatTensor]` of shape :obj:`(batch_size, sequence_len, bittensor.network_size)`, `required`): - Output encodings of images produced by remote neurons. Non-responses are zeroes of common shape. - - return_codes (:obj:`List[torch.LongTensor]` of shape :obj:`[num_neurons]`, `required`): - dendrite call return ops. - """ - # TODO(const): Checks across all tensors and other shape checks. - if len(x[0].shape) != 5: - error_msg = 'Image inputs should be rank 5 with semantic shape: [batch_size, sequence_dim, channels, rows, cols]' - raise ValueError(error_msg) - if len(x) != len(neurons): - error_msg = 'List of image inputs x should have the same length as passed destination neurons, got {} and {}'.format(len(x), len(neurons)) - raise ValueError(error_msg) - if len(x) < 1: - error_msg = 'Must pass more than 0 input for argument x, got {}'.format(len(x)) - raise ValueError(error_msg) - return self.forward(neurons, x, bittensor_pb2.Modality.IMAGE) - - def forward_tensor(self, neurons: List[bittensor_pb2.Neuron], - x: List[torch.Tensor]) -> Tuple[List[torch.Tensor], torch.Tensor]: - r""" Forward tensor inputs to neurons. - - Args: - neurons (:obj:`List[bittensor_pb2.Neuron]` of shape :obj:`(num_neurons)`, `required`): - List of remote neurons which match length of x. Tensors from x are sent forward to these neurons. - - x (:obj:`List[torch.Tensor]` of shape :obj:`(num_neurons * [batch_size, sequence_len, bittensor.__network_dim__])`, `required`): - List of tensors to send to corresponsing neurons. Tensors are of arbitrary type and - with shape [batch_size, sequence_len, bittensor.__network_dim__]. - - Returns: - forwad_output (:obj:`List[torch.FloatTensor]` of shape :obj:`num_neurons * (batch_size, sequence_len, bittensor.__network_dim__)]`, `required`): - Output encodings of tensors produced by remote neurons. Non-responses are zeroes of common shape. - - return_codes (:obj:`List[torch.LongTensor]` of shape :obj:`[num_neurons]`, `required`): - dendrite call return ops. - """ - if len(x[0].shape) != 3: - error_msg = 'Tensor inputs should be rank 3 with semantic shape: [batch_size, sequence_len, feature_len]' - raise ValueError(error_msg) - if len(x) != len(neurons): - error_msg = 'List of tensor inputs x should have the same length as passed destination neurons, got {} and {}'.format(len(x), len(neurons)) - raise ValueError(error_msg) - if x[0].shape[2] != bittensor.__network_dim__: - error_msg = 'Passed tensor must have last dimension {} got {}'.format(bittensor.__network_dim__, x[0].shape[2]) - raise ValueError(error_msg) - if len(x) == 0: - error_msg = 'Must pass more than 0 input for argument x, got {}'.format(len(x)) - raise ValueError(error_msg) - return self.forward(neurons, x, bittensor_pb2.Modality.TENSOR) - - def forward(self, neurons: List[bittensor_pb2.Neuron], - x: List[torch.Tensor], - mode: bittensor_pb2.Modality) -> Tuple[List[torch.Tensor], torch.LongTensor]: - r""" Forward tensor inputs to neurons. - - Args: - neurons (:obj:`List[bittensor_pb2.Neuron]` of shape :obj:`(num_neurons)`, `required`): - List of remote neurons which match length of x. Tensors from x are sent forward to these neurons. - - x (:obj:`List[torch.Tensor]` of shape :obj:`(num_neurons * [shape])`, `required`): - List of tensors to send to corresponsing neurons. Tensors are of arbitrary type and shape depending on the - modality. - - mode (:obj:`bittensor_pb2.Modality` of shape :obj:`(1)`, `required`): - Bittensor forward modality type. Enum in [TEXT, IMAGE, TENSOR] - - Returns: - forward_outputs (:obj:`List[torch.FloatTensor]` of shape :obj:`num_neurons * (batch_size, sequence_len, bittensor.network_size)]`, `required`): - Output encodings of tensors produced by remote neurons. Non-responses are zeroes of common shape. - - return_codes (:obj:`List[torch.LongTensor]` of shape :obj:`[num_neurons]`, `required`): - dendrite call return ops. - """ - if len(x) != len(neurons): - error_msg = 'List of inputs x should have the same length as passed destination neurons, got {} and {}'.format(len(x), len(neurons)) - raise ValueError(error_msg) - if len(x) < 1: - error_msg = 'Must pass more than 0 input for argument x, got {}'.format(len(x)) - raise ValueError(error_msg) - - # ---- Run async calls ---- - loop = asyncio.new_event_loop() - results = loop.run_until_complete(self._gather(loop, x, neurons, mode)) - loop.stop() - - # ---- Process results and return ---- - tensor_results = [res[0] for res in results] - return_codes = torch.tensor([res[1] for res in results]) - return tensor_results, return_codes - - async def _gather(self, loop: asyncio.base_events.BaseEventLoop, inputs, neurons, mode) -> List[Tuple[torch.FloatTensor, torch.LongTensor]]: - r""" Creates and returns the results from len(neurons) torch forward requests. Uses asyncio for concurrency. - - Args: - loop (:obj:`asyncio.base_events.BaseEventLoop`, `required`): - The asyncio concurrency loop to use while making the n calls. - - inputs (:obj:`List[torch.Tensor]` of shape :obj:`(num_neurons * [shape])`, `required`): - List of tensors to send to corresponsing neurons. Tensors are of arbitrary type and shape depending on the - modality. - - neurons (:obj:`List[bittensor_pb2.Neuron]` of shape :obj:`(num_neurons)`, `required`): - List of remote neurons which match length of x. Tensors from x are sent forward to these neurons. - - mode (:obj:`bittensor_pb2.Modality` of shape :obj:`(1)`, `required`): - Bittensor forward modality type. Enum in [TEXT, IMAGE, TENSOR] - - Returns: - results (:obj:`List[Tuple[torch.FloatTensor, torch.LongTensor]]`, `required`): - result tuples from the forward call on a RemoteNeuron class. - """ - - # ---- Calls to fill ---- - calls = [] - for (inputs_i, neuron_i) in list(zip(inputs, neurons)): - - # ---- Find remote or create one ---- - if neuron_i.public_key not in self._remotes: - self._remotes[neuron_i.public_key] = RemoteNeuron(neuron_i, self._config, self.__keypair) - remote = self._remotes[neuron_i.public_key] - - # ---- Append async calls ---- - calls.append( loop.run_in_executor(None, remote.forward, inputs_i, mode) ) - - # ---- Gather results and return ---- - results = await asyncio.gather(*calls) - return results - -class RemoteNeuron(nn.Module): - """ Class which bundles a grpc connection to a remote host as a standard auto-grad torch.nn.Module. - """ - - def __init__(self, neuron: bittensor_pb2.Neuron, config, keypair): - super().__init__() - self.neuron = neuron # Endpoint information. - self.config = config # Configuration i.e. rpc timeout. - self.keypair = keypair # Cryptographic keypair. - self.signature = None # Call signature. - self.nounce = None # Call nounce. - self.backoff = 0 # Number o queries to backoff. - self.next_backoff = 1 # Next backoff level. - self.stats = SimpleNamespace( - total_success_time = 0.0, - avg_success_time = 0.0, - success_bytes_out = 0.0, - success_bytes_in = 0.0, - codes = { - bittensor_pb2.ReturnCode.Success: 0, - bittensor_pb2.ReturnCode.Timeout: 0, - bittensor_pb2.ReturnCode.Backoff: 0, - bittensor_pb2.ReturnCode.Unavailable: 0, - bittensor_pb2.ReturnCode.NotImplemented: 0, - bittensor_pb2.ReturnCode.EmptyRequest: 0, - bittensor_pb2.ReturnCode.EmptyResponse: 0, - bittensor_pb2.ReturnCode.InvalidResponse: 0, - bittensor_pb2.ReturnCode.InvalidRequest: 0, - bittensor_pb2.ReturnCode.RequestShapeException: 0, - bittensor_pb2.ReturnCode.ResponseShapeException: 0, - bittensor_pb2.ReturnCode.RequestSerializationException: 0, - bittensor_pb2.ReturnCode.ResponseSerializationException: 0, - bittensor_pb2.ReturnCode.RequestDeserializationException: 0, - bittensor_pb2.ReturnCode.ResponseDeserializationException: 0, - bittensor_pb2.ReturnCode.NotServingSynapse: 0, - bittensor_pb2.ReturnCode.NucleusTimeout: 0, - bittensor_pb2.ReturnCode.NucleusFull: 0, - bittensor_pb2.ReturnCode.UnknownException: 0, - } - ) - # Loop back if the neuron is local. - if neuron.address == config.axon.remote_ip: - ip = "localhost:" - if config.axon.remote_ip == "host.docker.internal": - ip = "host.docker.internal:" - self.endpoint = ip + str(neuron.port) - else: - self.endpoint = neuron.address + ':' + str(neuron.port) - self.channel = grpc.insecure_channel( - self.endpoint, - options=[('grpc.max_send_message_length', -1), - ('grpc.max_receive_message_length', -1)]) - self.stub = bittensor_grpc.BittensorStub(self.channel) - - - def __del__(self): - if self.channel is not None: - self.channel.close() - - def forward(self, inputs: torch.Tensor, mode: bittensor_pb2.Modality) -> Tuple[torch.Tensor, int]: - r""" Torch.nn.Module forward call: Triggers the grpc call to the remote neuron on the associated endpoint. - Call returns the output tensor and a bittensor_pb2.ReturnCode. - - Args: - inputs (:obj:`List[torch.Tensor]` of shape :obj:`(shape)`, `required`): - Single torch tensor to be sent to the remote neuron endpoint. - - mode (:obj:`bittensor_pb2.Modality` of shape :obj:`(1)`, `required`): - Bittensor forward modality type. Enum in [TEXT, IMAGE, TENSOR] - - Returns: - output (:obj:`Tuple[torch.FloatTensor, torch.LongTensor]`, `required`): - Result tuple from the forward call. - - """ - # ---- On Backoff: We dont make an RPC and return zeros instead ---- - if self.config.dendrite.do_backoff and self.backoff >= 1: - outputs = nill_response_for(inputs) - code = torch.tensor(bittensor_pb2.ReturnCode.Backoff) - - # ---- On Not-backoff: We make the Forward RPC ---- - else: - try: - # Make and time the query. - start_time = time.time() - outputs, code = _RemoteModuleCall.apply(self, DUMMY, inputs, mode) - elapsed_time = time.time() - start_time - - # ---- On unknown failure: we return zeros and unknown code ---- - except Exception as e: - logger.error('Uncaught error in forward call with error {}'.format( e )) - outputs = nill_response_for(inputs) - code = torch.tensor(bittensor_pb2.ReturnCode.UnknownException) - - # ---- On Success: set zero backoff and halve the next backoff ---- - self.stats.codes[code.item()] += 1 - if code.item() == bittensor_pb2.ReturnCode.Success: - self.backoff = 0 - self.next_backoff = max(1, self.next_backoff / 2) - - # We only need to record elapsed times / bytes on success. - self.stats.total_success_time += elapsed_time - self.stats.avg_success_time = self.stats.total_success_time / self.stats.codes[bittensor_pb2.ReturnCode.Success] - self.stats.success_bytes_out += sys.getsizeof(inputs) - self.stats.success_bytes_in += sys.getsizeof(outputs) - - # ---- On Backoff: Lower backoff value by 1 ---- - elif code.item() == bittensor_pb2.ReturnCode.Backoff: - # We slowly lower the backoff count until 0. - self.backoff -= 1 - - # ---- On failure: Increase backoff and double next_backoff towards max value ---- - # Catch all non-success / non-backoff codes and trigger backoff increase. This catches - # serialization errors, timeouts, unavailable endpoints etc. Note, it can - # be triggered by invalid requests on this side of the query. - else: - # ---- Do backoff: incease backoff until max_backoff is reached ---- - self.backoff = self.next_backoff - self.next_backoff = min(self.config.dendrite.max_backoff, self.next_backoff * 2) - - # ---- Finally return ---- - return outputs, code - -class _RemoteModuleCall(torch.autograd.Function): - @staticmethod - def forward(ctx, caller: RemoteNeuron, dummy: torch.Tensor, inputs: torch.Tensor, mode: bittensor_pb2.Modality) -> Tuple[torch.Tensor, int]: - - """ Internal autograd-friendly Forward RPC call to a remote neuron (calls the Forward method on an Axon terminal.) - - Args: - ctx: (:obj:`torch.autograd.ctx`, `required`): - Autograd context, saves state information between forward and backward calls. i.e. inputs for gradient computation. - - caller: (:obj:`RemoteNeuron`, `required`): - Caller object the remote neuron containing the endpoint information, RPC channel etc. - - dummy: (:obj:`torch.Tensor`, `required`): - Dummy torch tensor used to ensure that torch.backward computation is called on this function - regardless of the input types. - - inputs (:obj:`List[torch.Tensor]` of shape :obj:`(shape)`, `required`): - Torch tensor to be sent to the caller associated endpoint neurons. - - mode (:obj:`bittensor_pb2.Modality` of shape :obj:`(1)`, `required`): - Bittensor forward modality type. Enum in [TEXT, IMAGE, TENSOR] - - Returns: - output (:obj:`Tuple[torch.FloatTensor`, torch.LongTensor]`, `optional`): - Result from forward call. May be None in the case of failure. - - code (:obj:`bittensor_pb2.ReturnCode`, `required`): - Return code associated with forward call. - """ - - # ---- Save for backward call ---- - ctx.caller = caller - ctx.mode = mode - ctx.inputs = inputs - - zeros = nill_response_for(inputs) - try: - # ---- Check inputs size ---- - if torch.numel(inputs) == 0: - return zeros, torch.tensor(bittensor_pb2.ReturnCode.EmptyRequest) - - # ---- Inputs Serialization ---- - try: - serialized_inputs = PyTorchSerializer.serialize(inputs, mode) - except Exception as e: - logger.warning('Serialization error with error {}', e) - return zeros, torch.tensor(bittensor_pb2.ReturnCode.RequestSerializationException) - ctx.serialized_inputs = serialized_inputs - - # ---- Build request ---- - request = bittensor_pb2.TensorMessage( - version=bittensor.__version__, - public_key=ctx.caller.keypair.public_key, - nounce=ctx.caller.nounce, - signature=ctx.caller.signature, - tensors=[serialized_inputs]) - - # ---- Make RPC call ---- - try: - response = ctx.caller.stub.Forward(request, timeout=caller.config.dendrite.timeout) - - # ---- Catch bittensor errors ---- - bittensor_code = response.return_code - if bittensor_code == bittensor_pb2.ReturnCode.UnknownException: - logger.error('Unknown exception returned from remote host with message {}', response.message) - return zeros, torch.tensor(bittensor_code) - - elif bittensor_code != bittensor_pb2.ReturnCode.Success: - return zeros, torch.tensor(bittensor_code) - - # ---- Catch GRPC Errors ---- - except grpc.RpcError as rpc_error_call: - grpc_code = rpc_error_call.code() - - if grpc_code == grpc.StatusCode.DEADLINE_EXCEEDED: - return zeros, torch.tensor(bittensor_pb2.ReturnCode.Timeout) - - elif grpc_code == grpc.StatusCode.UNAVAILABLE: - return zeros, torch.tensor(bittensor_pb2.ReturnCode.Unavailable) - - else: - logger.error('Uncaught GPRC error exception with code {} from endpoint {}', grpc_code, caller.endpoint) - return zeros, torch.tensor(bittensor_pb2.ReturnCode.UnknownException) - - # ---- Catch Unknown Errors ---- - except Exception as e: - logger.error('Uncaught error in forward call with error {} and endpoint', e, caller.endpoint) - return zeros, torch.tensor(bittensor_pb2.ReturnCode.Unknown) - - # ---- Check tensor response length ---- - if len(response.tensors) == 0: - return zeros, torch.tensor(bittensor_pb2.ReturnCode.EmptyResponse) - - # ---- Deserialize response ---- - try: - outputs = PyTorchSerializer.deserialize_tensor(response.tensors[0]) - except Exception as e: - logger.error('Failed to serialize responses from forward call with error {}', e) - return zeros, torch.tensor(bittensor_pb2.ReturnCode.ResponseDeserializationException) - - # ---- Check response shape ---- - if outputs.size(0) != inputs.size(0) \ - or outputs.size(1) != inputs.size(1) \ - or outputs.size(2) != bittensor.__network_dim__: - logger.error('Forward request returned tensor with incorrect shape {}', list(outputs.shape)) - return zeros, torch.tensor(bittensor_pb2.ReturnCode.ResponseShapeException) - - # ---- Safe catch NaNs and replace with 0.0 ---- - outputs = torch.where(torch.isnan(outputs), torch.zeros_like(outputs), outputs) - - # ---- Catch all ---- - except Exception as e: - logger.error('Forward request returned unknown error {}', e) - return zeros, torch.tensor(bittensor_pb2.ReturnCode.UnknownException) - - # ---- Return ---- - return outputs, torch.tensor(response.return_code) - - @staticmethod - @once_differentiable - def backward(ctx, grads: torch.FloatTensor, code: torch.FloatTensor) -> Optional[torch.Tensor]: - """ Internal autograd-friendly Backward RPC call to a remote neuron (calls the Backward method on an remote Axon terminal.) - - Args: - ctx: (:obj:`torch.autograd.ctx`, `required`): - Autograd context, saves state information between forward and backward calls. i.e. inputs for gradient computation. - - grads (:obj:`List[torch.Tensor]` of shape :obj:`(shape)`, `required`): - Gradients of this function's outputs computed during the loss.backward() call. - - code (:obj:`bittensor_pb2.Modality` of shape :obj:`(1)`, `required`): - Code output from the forward call. - - Returns: - output (:obj:`Tuple[torch.FloatTensor`, torch.LongTensor]`, `optional`): - Gradients of the inputs with respect to the inputs and grads of the outputs. - """ - # ---- Zeros response in the case of failure ---- - zeros = nill_response_for(ctx.inputs) - - # ---- Check if are passing gradients ---- - if not ctx.caller.config.dendrite.pass_gradients: - return (None, None, zeros, None) - - # ---- Check that forward query was a success ---- - if code.item() != bittensor_pb2.ReturnCode.Success: - return (None, None, zeros, None) - - # ---- Try to pass gradients ---- - else: - try: - # ---- Serialize inputs to bitensor_pb2.Tensor ---- - serialized_grads = PyTorchSerializer.serialize_tensor(grads) - serialized_inputs = ctx.serialized_inputs - - # ---- Build request for backward ---- - request = bittensor_pb2.TensorMessage( - version=bittensor.__version__, - public_key=ctx.caller.keypair.public_key, - nounce=ctx.caller.nounce, - signature=ctx.caller.signature, - tensors=[serialized_inputs, serialized_grads]) - - # --- Send non blocking grad request ---- - # NOTE(const): we dont care about the response. - ctx.caller.stub.Backward.future(request, timeout=ctx.caller.config.dendrite.timeout) - - # ---- Always return zeros ---- - # NOTE(const): We can return non zeros or a remote host could mess with your training - # without you knowing about it. - return (None, None, zeros, None) - - except: - - # ---- Catch all exceptions in Backward ---- - rollbar.send_exception() - return (None, None, zeros, None) - diff --git a/bittensor/dendrites/__pycache__/pkm.cpython-38.pyc b/bittensor/dendrites/__pycache__/pkm.cpython-38.pyc deleted file mode 100644 index fc399a7d45..0000000000 Binary files a/bittensor/dendrites/__pycache__/pkm.cpython-38.pyc and /dev/null differ diff --git a/bittensor/dendrites/pkm.py b/bittensor/dendrites/pkm.py deleted file mode 100644 index 5e60638e17..0000000000 --- a/bittensor/dendrites/pkm.py +++ /dev/null @@ -1,308 +0,0 @@ -import argparse -import torch -import torch.nn as nn -from torch.nn import functional as F -from typing import Tuple - -import bittensor -from bittensor import bittensor_pb2 - -class PKMKeys(nn.Module): - - def __init__(self, key_dim): - super().__init__() - self._key_dim = key_dim - self._n_keys = 10 # Initial size = 10 - self._keys = torch.rand(self._n_keys, self._key_dim) - - def forward(self, uids: torch.Tensor) -> torch.Tensor: - r""" Maps metagraph uids to torch keys - Args: - uids (:obj:`torch.LongTensor` of shape :obj:`(batch_size, sequence_dim, channels, rows, cols)`, `required`): - Image tensors to forward. - - Returns: - keys (:obj:`torch.FloatTensor` of shape :obj:`(batch_size, key_dim)`, `required`): - Torch key for each uid. - """ - # Get max value for possible resize. - max_uid = torch.max(uids) - if max_uid >= self._n_keys - 1: - new_keys = torch.rand( (max_uid - self._n_keys) + 10, self._key_dim) - self._keys = torch.cat([self._keys, new_keys], dim=0) - self._n_keys = self._keys.shape[0] - return self._keys[uids] - -class PKMDendrite(): - def __init__(self, config, session, query_dim): - self.config = config - self.session = session - self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu") - # UIDs -> Keys. - self.keys = PKMKeys(self.config.dendrite.key_dim) - # Query -> Keys - self.projection = nn.Linear(query_dim, self.config.dendrite.key_dim, bias=True).to(self.device) - - - @staticmethod - def add_args(parser: argparse.ArgumentParser) -> argparse.ArgumentParser: - parser.add_argument('--dendrite.key_dim', default=100, type=int, help='Product keys dimension.') - parser.add_argument('--dendrite.topk', default=10, type=int, help='Number of keys to select for each example.') - parser.add_argument('--dendrite.stale_emit_filter', default=10000, type=int, help='Number of blocks before a neuron is filtered without a recent emit') - return parser - - @staticmethod - def check_config(config): - return config - - def _route(self, inputs, query, modality: bittensor_pb2.Modality) -> Tuple[torch.FloatTensor, torch.FloatTensor, torch.LongTensor, torch.LongTensor]: - r""" Routes inputs using context and metagraph state. - - Args: - inputs (:obj:`torch.FloatTensor` of shape :obj:`(batch_size, *-1*)`, `required`): - tensors inputs to distribute to neurons using context. - - query (:obj:`torch.FloatTensor` of shape :obj:`(batch_size, query_dimension)`, `required`): - Context tensor used to select which neurons query for each example. - - modality (:obj:`bittensor_pb2.Modality` of shape :obj:`(1)`, `required`): - Bittensor forward modality type. Enum in [TEXT, IMAGE, TENSOR] - - Returns: - responses (:obj:`torch.FloatTensor` of shape :obj:`(batch_size, sequence_dim, bittensor.__network_dim__)`, `required`): - Joined responses from each queried neuron. - - weights (:obj:`torch.FloatTensor` of shape :obj:`(batch_size, metagraph.state.n)`, `required`): - weights for each neuron per example. - - requests_sizes (:obj:`torch.LongTensor` of shape :obj:`(metagraph.state.n)`, `required`): - number of requests sent to each uid in this batch. - - return_codes (:obj:`List[torch.LongTensor]` of shape :obj:`[num_neurons]`, `required`): - dendrite call return codes. - """ - # For ease of use. - batch_size = inputs.shape[0] - - - # all_uids: (torch.LongTensor): unique keys for each peer neuron. - # all_uids.shape = [metagraph.n] - all_uids = self.session.metagraph.uids # Returns a list of neuron uids. - - # filtered_uids: (torch.LongTensor): keys filtered by emit. - # all_uids.shape = [metagraph.n] - current_block = self.session.metagraph.block - lastemit = self.session.metagraph.lastemit - staleness = (current_block - lastemit) - filtered_uids = all_uids[torch.where(staleness < self.config.dendrite.stale_emit_filter)] - n_uids = torch.numel(filtered_uids) - - # Return if there are no uids to query - if n_uids == 0: - # Return nill responses. - n = self.session.metagraph.n - null_response = torch.zeros(size=(inputs.shape[0], inputs.shape[1], bittensor.__network_dim__)) - null_weights = torch.zeros(size=(inputs.shape[0], n)) - null_sizes = torch.zeros(n) - null_retops = torch.zeros(n) - return null_response, null_weights, null_sizes, null_retops - - # keys: (torch.FloatTensor): unique trainable torch keys for each uid - # keys.shape = [n_uids, config.dendrite.key_dim] - keys = self.keys( filtered_uids ).to(self.device) - - # query: (torch.FloatTensor): projection of the query on to the key dimension. - # query.shape = [batch_size, config.dendrite.key_dim] - # On Cuda if it's available. - query = self.projection( query ) - - # scores: (torch.FloatTensor): cartesian product between keys and projection. - # scores.shape = [batch_size, n_uids] - # These are all on Cuda if it's available. - scores = F.linear(query, keys, bias=None) - scores = F.softmax(scores, dim = 1) # Softmax scores - - # topk_scores: (torch.FloatTensor): topk scores per example - # topk_indices: (torch.LongTensor): topk indices per example - # topk_scores.shape = [batch_size, real_topk] - # topk_indices.shape = [batch_size, real_topk] - # These are all on Cuda if it's available. - real_topk = min( n_uids, self.config.dendrite.topk ) - topk_scores, topk_indices = scores.topk(real_topk, dim=1) - - # gates: (torch.FloatTensor): gated scores for uid per example. Zeros for non queried uids. - # gates.shape = [batch_size, n_uids] - zeros = torch.zeros(batch_size, n_uids).to(self.device) - gates = zeros.scatter(1, topk_indices, topk_scores) - gates = F.normalize(gates, p=1, dim=1) - - # non_zero_gates: (torch.FloatTensor): indices of non-zero gate values. - # non_zero_gates.shape = [numel(gates), 2] - # sorted_indices: (torch.FloatTensor): sorted indices along the first dimension i.e. indices ordered by row. - # sorted_uids.shape = [batch_size, n_uids] - non_zero_gates = torch.nonzero(gates) - sorted_uids, index_sorted_uids = torch.sort(non_zero_gates, dim = 0) - - # uids_index: torch.FloatTensor): batch index of sorted uids. - # uids_indes.shape = [topk * batch_size, 1] - _, uids_index = sorted_uids.split(1, dim=1) - - # batch_index: (torch.FloatTensor): batch index for each uid x example - # batch_index.shape = [topk * batch_size] - batch_index = sorted_uids[index_sorted_uids[:, 1], 0] - - # inputs_expanded: (torch.FloatTensor): expanded inputs to topk * batch_size - # inputs_expanded.shape = [topk * batch_size, -1] - inputs_expanded = inputs[batch_index] - - # request_sizes: List (int): number of examples per uid. - # len(part_sizes) = [n_uids] - request_sizes = list((gates != 0.0).sum(0).cpu().numpy()) - - # requests: List(torch.FloatTensor): examples for each uids - # requests.shape = n_uids * [-1, inputs.shape[1:]] - requests = torch.split(inputs_expanded, request_sizes, dim=0) - - # neurons: List[bittensor_pb2.Neuron]: endpoint information for filtered keys. - # neurons.shape = n_uids * [ bittensor_pb2.Neuron ] - neurons = self.session.metagraph.uids_to_neurons(filtered_uids) - - # responses: image responses from neurons. - # responses.shape = neurons.size * [-1, sequence_dim, __network_dim__] - if modality == bittensor_pb2.Modality.TEXT: - responses, retops = self.session.dendrite.forward_text(neurons, requests) - - elif modality == bittensor_pb2.Modality.IMAGE: - responses, retops = self.session.dendrite.forward_image(neurons, requests) - - elif modality == bittensor_pb2.Modality.TENSOR: - responses, retops = self.session.dendrite.forward_tensor(neurons, requests) - - else: - raise NotImplementedError - - # stitched: (torch.FloatTensor): responses joined along the first dimension. - # stitched.shape = [real_topk, sequence_dim, bittensor.__network_dim__] - # flat_stitched: (torch.FloatTensor): responses joined and flattened along the last dimension. - # flat_stitched.shape = [real_topk * batch_size, *inputs.shape[1:]] - stitched = torch.cat(responses, 0) - flat_stitched = torch.flatten(stitched, start_dim = 1).to(self.device) - - # gates_expanded: (torch.FloatTensor): gate values for each queried uid per example. - # gates_expanded.shape = [real_topk * batch_size, n_uids] - gates_expanded = gates[batch_index.flatten()] - - # nonzero_gates: (torch.FloatTensor): non-zero gating values for each example for each uid. - # nonzero_gates.shape = [real_topk * batch_size, 1] - nonzero_gates = torch.gather(gates_expanded, 1, uids_index) - - # flat_stitched: (torch.FloatTensor): responses multiplied by gate values. - # flat_stitched.shape = [real_topk * batch_size, *inputs.shape[1:]] - flat_stitched = flat_stitched.mul(nonzero_gates) - - # zeros: (torch.FloatTensor): zero for combined responses. - # zeros.shape = [batch_size, *inputs.shape[1:]] - zeros = torch.zeros(batch_size, flat_stitched.shape[1], requires_grad=True) - - # combined: (torch.FloatTensor): combine responses by adding them to the corresponsing batch index. - # combined = [batch_size, *inputs.shape[1:]] - combined = zeros.to(self.device).index_add(0, batch_index, flat_stitched.float()) - - # combined: (torch.FloatTensor): combined responses reshaped to correct dimension. - # combined = [batch_size, sequence_dim, bittensor.__network_dim__] - combined = combined.view(batch_size, inputs.shape[1], bittensor.__network_dim__) - - # indices: (torch.LongTensor): indices of uids queried during this forward call. - # indices = [batch_size, metagraph.n] - indices = self.session.metagraph.uids_to_indices(filtered_uids) - - # weights: (torch.LongTensor): weights scattered onto uids per example. - # weights.shape = [batch_size, metagraph.n] - weights = torch.zeros(inputs.shape[0], self.session.metagraph.n) - weights.to(self.device).scatter_(1, indices.to(self.device).repeat(batch_size, 1), gates) - - # filled_sizes: (torch.LongTensor): number of examples queried to each uid. - # filled_sizes.shape = [metagraph.n] - filled_request_sizes = torch.zeros(self.session.metagraph.n, dtype=torch.long) - filled_request_sizes.scatter_(0, indices, torch.tensor(request_sizes)) - - # Return. - return combined, weights, filled_request_sizes, retops - - - def forward_image(self, images: torch.FloatTensor, query: torch.FloatTensor) -> Tuple[torch.FloatTensor, torch.FloatTensor, torch.LongTensor]: - r""" Forwards images to connected neurons using the passed context to learn connectivity. - - Args: - images (:obj:`torch.FloatTensor` of shape :obj:`(batch_size, sequence_dim, channels, rows, cols)`, `required`): - Image tensors to forward. - - query (:obj:`torch.FloatTensor` of shape :obj:`(batch_size, context_dim)`, `required`): - query tensor used to select which neurons query for each example. - - Returns: - responses (:obj:`torch.FloatTensor` of shape :obj:`(batch_size, sequence_dim, bittensor.__network_dim__)`, `required`): - Joined responses from each queried neuron. - - weights (:obj:`torch.FloatTensor` of shape :obj:`(batch_size, metagraph.state.n)`, `optional`): - weights for each neuron per example. - - requests_sizes (:obj:`torch.LongTensor` of shape :obj:`(metagraph.state.n)`, `optional`): - number of requests sent to each uid in this batch. - - return_codes (:obj:`List[torch.LongTensor]` of shape :obj:`[num_neurons]`, `required`): - dendrite call return codes. - """ - return self._route(images, query, bittensor_pb2.Modality.IMAGE) - - def forward_text(self, text: torch.LongTensor, query: torch.FloatTensor) -> Tuple[torch.FloatTensor, torch.FloatTensor, torch.LongTensor]: - r""" Forwards text to connected neurons using the passed context to learn connectivity. - - Args: - text (:obj:`torch.LongTensor` of shape :obj:`(batch_size, sequence_dim)`, `required`): - tensor of tokenized sentences. - - query (:obj:`torch.FloatTensor` of shape :obj:`(batch_size, query_dim)`, `required`): - Context tensor used to select which neurons query for each example. - - Returns: - responses (:obj:`torch.FloatTensor` of shape :obj:`(batch_size, sequence_dim, bittensor.__network_dim__)`, `required`): - Joined responses from each queried neuron. - - weights (:obj:`torch.FloatTensor` of shape :obj:`(batch_size, metagraph.state.n)`, `optional`): - weights for each neuron per example. - - requests_sizes (:obj:`torch.LongTensor` of shape :obj:`(metagraph.state.n)`, `optional`): - number of requests sent to each uid in this batch. - - return_codes (:obj:`List[torch.LongTensor]` of shape :obj:`[num_neurons]`, `required`): - dendrite call return codes. - - """ - return self._route(text, query, bittensor_pb2.Modality.TEXT) - - - def forward_tensor(self, tensors: torch.FloatTensor, query: torch.FloatTensor) -> Tuple[torch.FloatTensor, torch.FloatTensor, torch.LongTensor]: - r""" Forwards tensors to connected neurons using the passed context to learn connectivity. - - Args: - tensors (:obj:`torch.FloatTensor` of shape :obj:`(batch_size, sequence_dim, bittensor.__network_dim__)`, `required`): - tensors sent to connected neurons. - - query (:obj:`torch.FloatTensor` of shape :obj:`(batch_size, query_dim)`, `required`): - Query tensor used to select which neurons query for each example. - - Returns: - responses (:obj:`torch.FloatTensor` of shape :obj:`(batch_size, sequence_dim, bittensor.__network_dim__)`, `required`): - Joined responses from each queried neuron. - - weights (:obj:`torch.FloatTensor` of shape :obj:`(batch_size, metagraph.state.n)`, `optional`): - weights for each neuron per example. - - requests_sizes (:obj:`torch.LongTensor` of shape :obj:`(metagraph.state.n)`, `optional`): - number of requests sent to each uid in this batch. - - return_codes (:obj:`List[torch.LongTensor]` of shape :obj:`[num_neurons]`, `required`): - dendrite call return codes. - """ - return self._route(tensors, query, bittensor_pb2.Modality.IMAGE) diff --git a/bittensor/exceptions/__pycache__/Exceptions.cpython-38.pyc b/bittensor/exceptions/__pycache__/Exceptions.cpython-38.pyc deleted file mode 100644 index 709e0363eb..0000000000 Binary files a/bittensor/exceptions/__pycache__/Exceptions.cpython-38.pyc and /dev/null differ diff --git a/bittensor/exceptions/handlers/__pycache__/__init__.cpython-38.pyc b/bittensor/exceptions/handlers/__pycache__/__init__.cpython-38.pyc deleted file mode 100644 index b4a61eb8e0..0000000000 Binary files a/bittensor/exceptions/handlers/__pycache__/__init__.cpython-38.pyc and /dev/null differ diff --git a/bittensor/exceptions/handlers/rollbar/__init__.py b/bittensor/exceptions/handlers/rollbar/__init__.py deleted file mode 100644 index 7445bca995..0000000000 --- a/bittensor/exceptions/handlers/rollbar/__init__.py +++ /dev/null @@ -1,87 +0,0 @@ -import os -from loguru import logger -import asyncio -import rollbar -from pathlib import Path -from loguru._handler import Message - - -class RollbarHandler(): - def __init__(self): - self.token = os.environ.get("ROLLBAR_TOKEN", False) - - def write(self, message): - if not self.token: - return - - record = message.record - level = record['level'].name - - if level == "WARNING": - rollbar.report_message(message, 'warning') - elif level == "ERROR": - rollbar.report_message(message, 'error') - else: - pass - - - - -def init(): - token = os.environ.get("ROLLBAR_TOKEN", False) - if not token: - return - - env = os.environ.get("BT_ENV", "production") - logger.info("Error reporting enabled using {}:{}", token, env) - rollbar.init(token, env) - set_runtime_status("OK") - - loop = asyncio.get_event_loop() - loop.set_exception_handler(asyncio_exception_handler) - - logger.add(sink=RollbarHandler(), level='WARNING') - - -def is_enabled(): - return os.environ.get("ROLLBAR_TOKEN", False) - -def set_runtime_status(status): - file = Path('/tmp/bt_runstate') - with file.open("w") as file: - file.write("%s\n" % status) - - -def asyncio_exception_handler(loop, context): - logger.debug("asyncio exception has occured") - - exception: BaseException - exception = context['exception'] - - exc_info = __get_exc_info(exception) - extra_data = __get_extra_data(context) - - send_exception(exc_info, extra_data) - logger.error(context) - -def __get_exc_info(exception): - _type = type(exception) - value = exception - tb = exception.__traceback__ - return _type, value, tb - -def __get_extra_data(context): - frames = [] - for elem in context['source_traceback']: - frames.append(str(elem)) - extra_data = "\n".join(frames) - return extra_data - -def send_exception(info = None, extra_data = None): - if not is_enabled(): - return - - logger.info("Sending exception to rollbar") - rollbar.report_exc_info(exc_info=info, extra_data=extra_data) - set_runtime_status("ERR") - diff --git a/bittensor/exceptions/handlers/rollbar/__pycache__/__init__.cpython-38.pyc b/bittensor/exceptions/handlers/rollbar/__pycache__/__init__.cpython-38.pyc deleted file mode 100644 index 65216cbce1..0000000000 Binary files a/bittensor/exceptions/handlers/rollbar/__pycache__/__init__.cpython-38.pyc and /dev/null differ diff --git a/bittensor/metagraph.py b/bittensor/metagraph.py deleted file mode 100644 index d2c847f312..0000000000 --- a/bittensor/metagraph.py +++ /dev/null @@ -1,909 +0,0 @@ - -import asyncio -import copy -import argparse -import bittensor -import math -import netaddr -import numpy -import time -import torch -import traceback - -from munch import Munch -from loguru import logger -from bittensor import bittensor_pb2 -from bittensor.subtensor.client import WSClient -from typing import List, Tuple, List - -from bittensor.exceptions.handlers import rollbar - -MAX_INT_WEIGHT = 4294967295 # Max weight value on chain. - -def int_to_ip(int_val): - return str(netaddr.IPAddress(int_val)) - -def ip_to_int(str_val): - return int(netaddr.IPAddress(str_val)) - -class ChainState(): - def __init__(self): - # Cached values. - self.n = 0 - self.next_uid = 0 - self.uids = [] - self.stake = [] - self.lastemit = [] - self.neuron_weights = [] - self.weight_uids = [] - self.weight_vals = [] - self.neurons = [] - self.index_for_uid = {} - self.index_for_pubkey = {} - self.pubkey_for_index = {} - - def add_or_update(self, pubkey:str, ip: int, port: int, uid: int, ip_type: int, lastemit: int, stake: int, w_uids: List[str], w_vals: List[int]): - neuron = bittensor_pb2.Neuron( - version = bittensor.__version__, - public_key = pubkey, - address = int_to_ip(ip), - port = int(port), - ip_type = int(ip_type), - uid = int(uid), - ) - if pubkey in self.index_for_pubkey: - index = self.index_for_pubkey[pubkey] - self.neurons[index] = neuron - self.stake[index] = float(stake) - self.lastemit[index] = int(lastemit) - self.weight_uids[index] = list(w_uids) - self.weight_vals[index] = list(w_vals) - self.uids[index] = int(uid) - else: - index = self.n - self.n += 1 - self.index_for_pubkey[pubkey] = index - self.pubkey_for_index[index] = pubkey - self.neurons.append(neuron) - self.stake.append(float(stake)) - self.lastemit.append(int(lastemit)) - self.weight_uids.append(list(w_uids)) - self.weight_vals.append(list(w_vals)) - self.uids.append( uid ) - self.index_for_uid[uid] = index - -# Static network state object. -class TorchChainState(): - r""" Maintains the chain state as a torch object. - Params: - tau: (int): - current, per block, token inflation rate. - - block: (int): - state block number. - - uids: (:obj:`torch.LongTensor` of shape :obj:`(metagraph.n)`): - UIDs for each neuron ordered by index. - - indices: (:obj:`torch.LongTensor` of shape :obj:`(metagraph.n)`): - Index of neurons, range(metagraph.n) - - stake: (:obj:`torch.LongTensor` of shape :obj:`(metagraph.n)`): - Stake balance for each neuron ordered by index. - - lastemit: (:obj:`torch.LongTensor` of shape :obj:`(metagraph.n)`): - Last emission call for each neuron ordered by index. - - weights: (:obj:`torch.FloatTensor` of shape :obj:`(metagraph.n)`): - This neuron's weights W[,:] - - W: (:obj:`torch.FloatTensor` of shape :obj:`(metagraph.n, metagraph.n)`): - Full weight matrix on chain. - - neurons: (List[bittensor_pb2.Neuron]) - List of endpoints on the network. - - """ - def __init__(self): - self.tau = torch.tensor([50.0], dtype = torch.float32) - self.block = 0 - self.n = 0 - self.uids = torch.tensor([]) - self.indices = torch.tensor([]) - self.stake = torch.tensor([]) - self.lastemit = torch.tensor([]) - self.W = torch.tensor([[]]) - self.neurons = [] - self.uid_for_pubkey = {} - self.index_for_uid = {} - - @staticmethod - def from_cache(cache: ChainState): - r""" Deep copies from the chain state. - """ - # Deep copies chain state into metagraph state. - state = TorchChainState() - state.n = cache.n - state.tau = torch.tensor([50.0], dtype = torch.float32) - state.neurons = copy.deepcopy(cache.neurons) - state.indices = torch.tensor(range(state.n), dtype=torch.int64) - state.uids = torch.tensor(copy.deepcopy(cache.uids), dtype=torch.int64) - state.lastemit = torch.tensor(copy.deepcopy(cache.lastemit), dtype=torch.int64) - state.stake = torch.tensor(copy.deepcopy(cache.stake), dtype=torch.float32) - for idx, (uid, n) in enumerate(list(zip(cache.uids, cache.neurons))): - state.uid_for_pubkey[n.public_key] = uid - state.index_for_uid[uid] = idx - weights_numpy = numpy.zeros( (state.n, state.n) ) - for i in range(state.n): - uids = cache.weight_uids[i] - vals = cache.weight_vals[i] - val_sum = sum(vals) - for uid, val in list(zip(uids, vals)): - if uid in cache.index_for_uid: - j = cache.index_for_uid[uid] - weights_numpy[i, j] = float(val) / float(val_sum) - state.W = torch.tensor(weights_numpy, dtype=torch.float32) - return state - -class Metagraph(): - - def __init__(self, config): - r"""Initializes a new Metagraph subtensor interface. - Args: - config (bittensor.Config): - An bittensor config object. - """ - # Protected vars - self._config = config - self.__keypair = config.session.keypair - - # Client for talking to chain. - self.subtensor_client = WSClient(self._config.metagraph.chain_endpoint, self.__keypair) - - # This neurons metadata on chain, initially None, filled on subscribe. - self.metadata = None - - # Chain state cached before converted into the torch state. - self.cache = ChainState() - - # Chain state as torch values. - self.state = TorchChainState.from_cache(self.cache) - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - # TODO(const): check this endpoint in check_config. - parser.add_argument('--metagraph.chain_endpoint', default='206.189.254.5:12345', type=str, - help='chain endpoint.') - parser.add_argument('--metagraph.stale_emit_filter', default=10000, type=int, - help='filter neurons with last emit beyond this many blocks.') - - @staticmethod - def check_config(config: Munch): - pass - - @property - def n(self) -> int: - r""" Return the number of known neurons on chain. - Returns - n: (int): - number of known neurons. - """ - return self.state.n - - @property - def block(self) -> int: - r""" Return the block number when the chain state was updated. - Returns - block: (int): - local chain state block number. - """ - return self.state.block - - @property - def lastemit(self) -> torch.LongTensor: - r""" Returns the last emit time for each known neuron. - Returns - lastemit: (int): - last emit time. - """ - return self.state.lastemit - - @property - def indices(self) -> torch.LongTensor: - r""" Return the indices of each neuron in the chain state range(metagraph.n). - Returns - indices: (:obj:`torch.LongTensor` of shape :obj:`(metagraph.n)`): - returned indices for each neuron. - """ - return self.state.indices - - @property - def uids(self) -> torch.LongTensor: - r""" Returns unique ids for each neuron in the chain state. - Returns - uids: (:obj:`torch.LongTensor` of shape :obj:`(metagraph.n)`): - unique id for each neuron. - """ - return self.state.uids - - @property - def stake(self) -> torch.FloatTensor: - r""" Returns the stake held by each known neuron. - Returns - stake: (:obj:`torch.FloatTensor` of shape :obj:`(metagraph.n)`): - stake of each known neuron. - """ - return self.state.stake - - @property - def S(self) -> torch.FloatTensor: - r""" Returns the stake held by each known neuron. - Returns - S: (:obj:`torch.FloatTensor` of shape :obj:`(metagraph.n)`): - stake of each known neuron. - """ - return self.state.stake - - @property - def tau(self) -> torch.FloatTensor: - r""" tau: the chain per block inflation rate. i.e. 50 - Returns - tau: (:obj:`torchFloatTensor` of shape :obj:`(1)`): - current chain inflation rate. - """ - return self.state.tau - - @property - def incentive(self) -> torch.FloatTensor: - r""" Returns the ranks - Returns - incentive: (:obj:`torch.FLoatTensor` of shape :obj:`(metagraph.n)`): - inflation incentive of each each known neuron. - """ - I = (self.tau * self.ranks) / torch.sum(self.ranks) - I = torch.where(torch.isnan(I), torch.zeros_like(I), I) - return I - - @property - def I(self) -> torch.FloatTensor: - r""" Returns the inflation incentive for each peer per block. - Returns - I: (:obj:`torch.FloatTensor` of shape :obj:`(metagraph.n)`): - stake of each known neuron. - """ - return self.incentive - - @property - def ranks(self) -> torch.FloatTensor: - r""" Returns the ranks W^t * S - Returns - ranks: (:obj:`torch.FloatTensor` of shape :obj:`(metagraph.n)`): - rank of each known neuron. - """ - if self.W.shape[0] == 0: - return torch.tensor([]) - else: - # S.shape = [self.state.n] - # W.shape = [self.state.n, self.state.n] - # R.shape = [self.state.n] - S = self.S.view(self.state.n, 1) - W = torch.transpose(self.W.view(self.state.n, self.state.n), 0, 1) - R = torch.matmul(W, S).view(self.state.n) - return R - - @property - def R(self) -> torch.FloatTensor: - r""" Returns ranks for each known neuron in the graph. - Returns - rank: (:obj:`torch.FloatTensor` of shape :obj:`(metagraph.n)`): - rank of each known neuron. - """ - return self.ranks() - - @property - def W(self) -> torch.FloatTensor: - r""" Full chain weight matrix for each neuron. - Returns - W: (:obj:`torch.LongFloat` of shape :obj:`(metagraph.n, metagraph.n)`): - w_ij of each neuron. - """ - return self.state.W - - @property - def neurons(self) -> List[bittensor_pb2.Neuron]: - r""" Return neuron endpoint information for each neuron. - Returns - neurons: (:obj:`List[bittensor_pb2.Neuron]` of shape :obj:`(metagraph.n, metagraph.n)`): - endpoint information for each neuron. - """ - return self.state.neurons - - @property - def public_keys(self) -> List[str]: - r""" Return the ordered public keys for state neurons. - Returns - public_keys: (:obj:`List[str]` of shape :obj:`(metagraph.n)`): - public keys of all graph neurons. - """ - return [n.public_key for n in self.state.neurons] - - @property - def weights(self) -> torch.FloatTensor: - r"""Return this neuron's weights. W[0,:] - Returns - weights: (:obj:`torch.FloatTensor` of shape :obj:`(metagraph.n)`): - returned indices for passed uids. - """ - if self.state.n == 0: - return torch.Tensor([]) - else: - w_0 = self.state.W[0,:] - return w_0 - - def uids_to_indices(self, uids: torch.Tensor): - r"""Return the indices of passed uids - Args: - uids: (:obj:`torch.LongTensor` of shape :obj:`(-1)`): - UIDs for indices - Returns - indices: (:obj:`torch.LongTensor` of shape :obj:`(-1)`): - returned indices for passed uids. - """ - indices = torch.nonzero(uids[..., None] == self.state.uids)[:,1] - if torch.numel(uids) != torch.numel(indices): - raise ValueError('Passed uids are not a subset of class.uids, with passed: {} and class.uids: {}'.format(uids, self.state.uids)) - return indices - - def uids_to_neurons(self, uids: torch.Tensor) -> List[bittensor_pb2.Neuron]: - r""" Returns a list with neurons for each uid. - Args: - uids: (torch.LongTensor) - uids into neuron protos - Returns: - neurons: (List[bittensor_pb2.Neuron]): - neuron info ordered by passed uids. - """ - response = [] - indices = self.uids_to_indices(uids) - for idx in indices.tolist(): - response.append(self.state.neurons[idx]) - return response - - def neurons_to_uids(self, neurons: List[bittensor_pb2.Neuron]) -> torch.LongTensor: - r""" Returns uids associated with the passed neurons. - Args: - neurons: (List[bittensor_pb2.Neuron]): - neuron info ordered by passed uids. - Returns: - uids: (torch.LongTensor) - uids associated with neurons. - """ - uids = [] - for n in neurons: - uids.append(self.state.uid_for_pubkey[n.public_key]) - return torch.tensor(uids) - - def chain_weights(self) -> torch.FloatTensor: - r""" Returns your current weights from the chain. - Returns: - weights: (:obj:`torch.FloatTensor` of shape :obj:`(-1)`): - weights on chain as torch tensor. - """ - loop = asyncio.get_event_loop() - return loop.run_until_complete(self.async_chain_weights()) - - async def async_chain_weights(self) -> torch.FloatTensor: - r""" Async: returns your current weights from the chain. - Returns: - weights: (:obj:`torch.FloatTensor` of shape :obj:`(-1)`): - weights on chain as torch tensor. - """ - # --- Get chain weights ---- - chain_uids = await self.subtensor_client.weight_uids(self.metadata['uid']) - chain_weights = await self.subtensor_client.weight_vals(self.metadata['uid']) - if chain_weights == None or len(chain_weights) == 0: - return torch.tensor([]) - - else: - # ---- To be filled ---- - return_val = torch.zeros(self.state.n) - - weight_sum = sum(chain_weights) - if weight_sum != MAX_INT_WEIGHT: - logger.error('Chain weights do not sum to {} with vals {}', MAX_INT_WEIGHT, chain_weights) - - # ---- Fill torch tensor ---- - for uid, weight in list(zip(chain_uids, chain_weights)): - if uid not in self.state.index_for_uid: - logger.critical('uid {} on chain not in state.index', uid) - continue - else: - idx = self.state.index_for_uid[ uid ] - if idx >= self.state.n: - logger.critical('idx {} in state.index > state.n', idx) - continue - else: - return_val[idx] = float(weight) / float(weight_sum) - return return_val - - def chain_block(self): - r""" Returns the current block on the chain. - Returns: - block: (int) block number on chain. - """ - loop = asyncio.get_event_loop() - loop.set_debug(enabled=True) - return loop.run_until_complete(self.async_chain_block()) - - async def async_chain_block(self) -> int: - r""" Async returns the current block on the chain. - Returns: - block: (int) block number on chain. - """ - return await self.subtensor_client.get_current_block() - - def unsubscribe(self) -> bool: - r""" Syncronous: Unsubscribes the local neuron from the chain. - """ - loop = asyncio.get_event_loop() - loop.set_debug(enabled=True) - return loop.run_until_complete(self.async_unsubscribe()) - - async def async_unsubscribe (self): - r""" Async: Unsubscribes the local neuron from the chain. - """ - logger.info('Unsubscribe from chain endpoint') - await self.subtensor_client.unsubscribe() - - def connect(self) -> bool: - r""" Synchronous: Connects to the chain. - Returns: - connected: (bool): true if the connection is a success. - """ - loop = asyncio.get_event_loop() - loop.set_debug(enabled=True) - return loop.run_until_complete(self.async_connect()) - - async def async_connect(self) -> bool: - r""" Async: Makes and awaits for a connection to the chain. - Returns: - connected: (bool): true if the connection is a success. - """ - self.subtensor_client.connect() - connected = await self.subtensor_client.is_connected() - return connected - - def sync(self) -> torch.FloatTensor: - r""" Synchronizes the local self.state with the chain state. - Returns: - weights: (torch.FloatTensor): - weights on chain. - """ - loop = asyncio.get_event_loop() - loop.set_debug(enabled=True) - return loop.run_until_complete(self.async_sync()) - - async def async_sync(self) -> torch.FloatTensor: - r""" Async: Synchronizes the local self.state with the chain state by polling the chain. - """ - await self._sync_cache() - last_sync = await self.async_chain_block() - self.state = TorchChainState.from_cache(self.cache) - self.state.block = last_sync - - async def _sync_cache(self): - r""" Async: Makes calls to chain updating local chain cache with newest info. - """ - # Make asyncronous calls to chain filling local state cache. - calls = [] - current_block = await self.async_chain_block() - neurons = await self.subtensor_client.neurons() - for (pubkey, neuron) in neurons: - last_emit = await self.subtensor_client.get_last_emit_data_for_uid(neuron['uid']) - if last_emit: - if (current_block - last_emit) < self._config.metagraph.stale_emit_filter: - calls.append(self._poll_pubkey(neuron, pubkey)) - await asyncio.gather(*calls) - - async def _poll_pubkey(self, neuron, pubkey): - r""" Polls info info for a specfic public key. - """ - try: - stake = await self.subtensor_client.get_stake_for_uid(neuron['uid']) - lastemit = await self.subtensor_client.get_last_emit_data_for_uid(neuron['uid']) - w_uids = await self.subtensor_client.weight_uids_for_uid(neuron['uid']) - w_vals = await self.subtensor_client.weight_vals_for_uid(neuron['uid']) - self.cache.add_or_update(pubkey = pubkey, ip = neuron['ip'], port = neuron['port'], uid = neuron['uid'], ip_type = neuron['ip_type'], lastemit = lastemit, stake = stake.rao, w_uids = w_uids, w_vals = w_vals) - except Exception as e: - logger.error("Exception occurred: {}".format(e)) - traceback.print_exc() - - - def subscribe(self, timeout) -> Tuple[int, str]: - r""" Syncronous: Makes a subscribe request to the chain. Waits for subscription inclusion or returns False - Returns: - see: _try_async_subscribe - """ - loop = asyncio.get_event_loop() - loop.set_debug(enabled=True) - return loop.run_until_complete(self.async_subscribe(timeout)) - - - SubscribeSuccess = 1 - SubscribeUnknownError = 2 - SubscribeTimeout = 3 - async def async_subscribe (self, timeout) -> Tuple[int, str]: - r""" Async: Makes a subscribe request to the chain. Waits for subscription inclusion or returns False - Returns: - see: _try_async_subscribe - """ - - # ---- Try Subscription ---- - try: - code, message = await self._try_async_subscribe(timeout) - - if code == Metagraph.SubscribeSuccess: - self.metadata = await self.subtensor_client.neurons(self.__keypair.public_key) - logger.info('Successfully subcribed session with: {}', self.metadata) - return code, message - - elif code == Metagraph.SubscribeUnknownError: - logger.error('Subscription threw an unknown error: {}', message) - return code, message - - elif code == Metagraph.SubscribeTimeout: - logger.error('Subscription timeout {}', message) - return code, message - - except Exception as e: - logger.error('Subscription threw an uncaught error {}', e) - return code, e - - async def _try_async_subscribe(self, timeout: int): - r""" Makes subscription attempts to the chain, continuing to attempt until timeout and finally waiting for inclusion. - - Args: - timeout (int): - Time to wait before subscription times out. - - Raises: - code (ENUM) {} - SubscribeSuccess: - Raised when the subscription is a success before the timeout - - SubscribeUnknownError: - UnknownError during subscription. - - SubscribeTimeout: - Raised when the attempted subscription fails after timeout. - } - message: - Message associated with code. - """ - start_time = time.time() - # ---- Make Subscription transaction ---- - while True: - try: - await self.subtensor_client.subscribe(self._config.axon.remote_ip, self._config.axon.port, self._config.session.coldkey) - break - - except Exception as e: - if (time.time() - start_time) > timeout: - # --- Timeout during emit call ---- - message = "Timed-out with Unknown Error while trying to make the subscription call. With last exception {}".format(e) - return Metagraph.SubscribeUnknownError, message - - else: - # --- Wait for inclusion, no error. - logger.trace('Error while attempting subscription {}', e) - continue - - # ---- Wait for inclusion ---- - while True: - try: - # ---- Request info from chain ---- - metadata = await self.subtensor_client.neurons(self.__keypair.public_key) - - except Exception as e: - # ---- Catch errors in request ---- - message = "Subscription threw an unknown exception {}".format(e) - return Metagraph.SubscribeUnknownError, message - - if metadata != None: - # ---- Subscription was a success ---- - return Metagraph.SubscribeSuccess, "Subscription success" - - elif time.time() - start_time > timeout: - # ---- wait ----- - return Metagraph.SubscribeTimeout, "Subscription timeout" - - else: - # ---- wait ----- - await asyncio.sleep(1) - - # ---- ?! WaT ?! ---- - logger.critical('Should not get here') - return Metagraph.SubscribeUnknownError, 'Should not get here' - - - def emit(self, weights: torch.FloatTensor, wait_for_inclusion = False, timeout = 12): - r""" Emits the passed weights to the chain. Optionally Waits for inclusion. - Failures are logged but do not break the process. - - Args: - Weights: (:obj:`torch.FloatTensor` of shape :obj:`(metagraph.n)`): - weights to set on chain of length self.state.n - - Wait_for_inclusion: (bool, default: False): - if true, the call waits for inclusion in the block before continuing. - - Timeout: (int, default = 12 sec): - time to wait for inclusion before raising a caught error. - """ - loop = asyncio.get_event_loop() - loop.set_debug(enabled=True) - loop.run_until_complete(self.async_emit(weights, wait_for_inclusion, timeout)) - - - EmitSuccess = 1 - EmitValueError = 2 - EmitUnknownError = 3 - EmitTimeoutError = 4 - EmitTimeoutError = 5 - EmitResultUnknown = 6 - EmitNoOp = 7 - async def async_emit(self, weights: torch.FloatTensor, wait_for_inclusion = False, timeout = 12) -> Tuple[int, str]: - r""" Calls _try_async_emit, logs and returns results based on code. Only fails on an uncaught Exception. - - Args: - weights: (:obj:`torch.FloatTensor` of shape :obj:`(metagraph.n)`): - Weights to set on chain. - wait_for_inclusion: (bool): - If true, the call waits for block-inclusion before continuing or throws error after timeout. - timeout: (int, default = 12 sec): - Time to wait for inclusion before raising a caught error. - - Returns: - see: _try_async_emit - """ - # --- Try emit, optionally wait ---- - try: - code, message= await self._try_async_emit(weights, wait_for_inclusion, timeout) - if code == Metagraph.EmitSuccess: - # ---- Emit was a success. ---- - logger.info("Successful emission.") - - elif code == Metagraph.EmitValueError: - # ---- Passed weights were incorrect ---- - logger.warning("Value error during emission: {}", message) - - elif code == Metagraph.EmitUnknownError: - # ---- Unknown error ---- - logger.error("Unknown error during emission: {}", message) - - elif code == Metagraph.EmitTimeoutError: - # ---- Timeout while waiting for inclusion ---- - logger.warning("Emission timeout after {} seconds with error {}", timeout, message) - - elif code == Metagraph.EmitResultUnknown: - # ---- Did not wait, result unknown ---- - logger.trace("Emit results unknown.") - - elif code == Metagraph.EmitNoOp: - # ---- Emit is a NoOp ---- - logger.info("When trying to set weights on chain. Weights are unchanged, nothing to emit.") - - except Exception as e: - # ---- Unknown error, raises error again. Should never get here ---- - logger.error("Unknown Error during emission {}", e) - raise e - - return code, message - - - async def _try_async_emit(self, weights: torch.FloatTensor, wait_for_inclusion = False, timeout = 12) -> Tuple[int, str]: - r""" Makes emit checks, emits to chain, and raises one of the following errors. - Args: - weights: (:obj:`torch.FloatTensor` of shape :obj:`(metagraph.n)`): - Weights to set on chain. - - wait_for_inclusion: (bool): - If true, the call waits for block-inclusion before continuing or throws error after timeout. - - timeout: (int, default = 12 sec): - Time to wait for inclusion before raising a caught error. - - Returns: - code (ENUM) { - EmitSuccess (ENUM): - Raised when try_async_emit emits weights successfully with known result. - - EmitNoOp (ENUM): - Raised when calling emit does not change weights on chain. - - EmitUnknownError (ENUM): - UnknownError during emit. - - EmitValueError (ENUM): - Raised during emission when passed weights are not properly set. - - EmitTimeoutError (ENUM): - Raised during emission during a timeout. - - EmitResultUnknown (ENUM): - Called when an emit step end without a known result, for instance, - if the user has wait_for_inclusion = False. - } - message: - Message associated with code. - """ - # --- Check type ---- - if not isinstance(weights, torch.Tensor): - message = "Error trying to set weights on chain. Got weights type {}, but weights must be of type {}".format(type(weights), torch.Tensor) - return Metagraph.EmitValueError, message - - # ---- Convert weights to list ---- - weights = [float(w) for w in weights.tolist()] - - # ---- Check length > 0 ---- - if len(weights) == 0: - message = "Error tyring to set weight on china. Got a length 0 set of values, must be at least length 1." - return Metagraph.EmitValueError, message - - # ---- Check length ---- - if len(weights) != self.state.n: - message = "Error trying to set weights on chain. Got length {}, but the length must match the number of neurons in metagraph.neurons {}".format(len(weights), self.state.n) - return Metagraph.EmitValueError, message - - # ---- Check approximate sum ---- - sum_weights = sum(weights) - epsilon = 0.001 - if abs(1.0 - sum_weights) > epsilon: - message = "Error trying to set weights on chain. Got {} for sum, but passed weights must sum to 1 ".format(len(sum_weights), self.state.n) - return Metagraph.EmitValueError, message - - # ---- Check min ---- - min_weights = min(weights) - if min_weights < 0.0: - message = "Error trying to set weights on chain. Got min value {} but values must be in range [0,1]".format(min_weights) - return Metagraph.EmitValueError, message - - # ---- Check max ---- - max_weights = max(weights) - if max_weights > 1.0: - message = "Error trying to set weights on chain. Got max value {} but values must be in range [0,1]".format(max_weights) - return Metagraph.EmitValueError, message - - # ---- Convert Weights to int-vals and pubkeys ---- - try: - weight_uids, weight_vals = self._convert_weights_to_emit(weights) - except Exception as e: - message = "Unknown error when converting weights to ints with weights {} and error {}".format(weight_vals, e) - return Metagraph.EmitUnknownError, message - - # ---- Check sum ---- - weight_sum = sum(weight_vals) - if weight_sum != MAX_INT_WEIGHT: - message = "Error trying to set weights on chain. Converted weights do not sum to {} with weights_vals {}".format(MAX_INT_WEIGHT, weight_vals) - return Metagraph.EmitValueError, message - - # ---- Check NO-OP ---- - if await self._are_set_on_chain(weight_vals, weight_uids): - message = "When trying to set weights on chain. Weights are unchanged, nothing to emit." - return Metagraph.EmitNoOp, message - - # ---- Emit ---- - start_time = time.time() - while True: - try: - # --- Make emission call ---- - logger.debug('Emit -> {} {}', weight_uids, weight_vals) - await self.subtensor_client.emit(weight_uids, weight_vals, self.__keypair, wait_for_inclusion = False) - break - - except Exception as e: - logger.trace('Emit error {}', e) - - if not wait_for_inclusion: - # --- No wait, and error during emit call ---- - message = "Error raised during call to emit {}".format( e ) - return Metagraph.EmitUnknownError, message - - elif (time.time() - start_time) > timeout: - # --- Timeout during emit call ---- - message = "Timed-out with unknown Error while trying to make the emit call. With last exception {}".format(e) - return Metagraph.EmitUnknownError, message - - else: - # --- Wait for inclusion, no error. - logger.info('retry emit...') - await asyncio.sleep(3) # To avoid ddos-ing the chain. - continue - - # --- Wait for inclusion ---- - if not wait_for_inclusion: - message = "Emit ended but we don't know if weights were set on chain" - return Metagraph.EmitResultUnknown, message - - else: - while True: - did_emit = await self._are_set_on_chain(weight_uids, weight_vals) - - if not did_emit and (time.time() - start_time) > timeout: - # ---- Emit caused timeout ----- - message = "Timed-out while waiting for inclusion." - return Metagraph.EmitTimeoutError, message - - elif not did_emit: - # ---- Did not emit, but waiting for inclusion ----- - await asyncio.sleep(3) - continue - - else: - # --- Did emit, return latest chain weights ---- - messages = "Successful emission" - return Metagraph.EmitSuccess, messages - - message = 'Should never get here' - logger.critical(message) - return Metagraph.EmitUnknownError, message - - async def _are_set_on_chain(self, weight_uids, weight_vals) -> bool: - r""" Returns true if the passed key and vals are set on chain. - """ - cmap = {} - chain_uids = await self.subtensor_client.weight_uids_for_uid(self.metadata['uid']) - chain_vals = await self.subtensor_client.weight_vals_for_uid(self.metadata['uid']) - - logger.info('chain_uids {}, chain_vals {}', chain_uids, chain_vals) - logger.info('weight_uids {}, weight_vals {}', weight_uids, weight_vals) - if chain_uids != None and chain_vals != None: - n_same = 0 - for uid, val in list(zip(chain_uids, chain_vals)): - cmap[uid] = val - for uid, val in list(zip(weight_uids, weight_vals)): - logger.info(uid) - if uid in cmap: - if cmap[uid] == val: - logger.info('same') - n_same += 1 - if n_same == len(weight_vals): - return True - else: - return False - else: - return False - - - def _convert_weights_to_emit(self, weights: List[float]) -> Tuple[List[str], List[int]]: - r""" Converts weights into integer u32 representation that sum to MAX_INT_WEIGHT. - Returns: - keys: (List[str]): - List of pubkeys associated with each weight from vals. - vals: (List[int]): - List of u32 integer representations of floating point weights. - """ - remainder = MAX_INT_WEIGHT - weight_vals = [] - weight_uids = [] - for i, val in enumerate(weights): - int_val = int(float(val) * int(MAX_INT_WEIGHT)) # convert to int representation. - remainder -= int_val - - # ---- Fix remainders and overflows ---- - if remainder < 0: - int_val = int_val + remainder - remainder = 0 - - if i == (len(weights) -1) and remainder > 0: # last item. - int_val += remainder - remainder = 0 - - # Do not add zero values. - if int_val != 0: - weight_vals.append( int_val ) # int weights sum to MAX_INT_WEIGHT. - weight_uids.append( self.state.uids.tolist()[i] ) # Gets the uid at this index - - return weight_uids, weight_vals - - diff --git a/bittensor/nucleus.py b/bittensor/nucleus.py deleted file mode 100644 index ecc69829f6..0000000000 --- a/bittensor/nucleus.py +++ /dev/null @@ -1,214 +0,0 @@ -import argparse -import sys -import torch -import traceback -import queue - -from loguru import logger -from munch import Munch -from typing import List, Tuple - -from bittensor import bittensor_pb2 -from bittensor.synapse import Synapse -from bittensor.utils.ptp import ThreadPoolExecutor - -class Nucleus (): - r""" Processing core of a bittensor Neuron. Runs behind an Axon endpoint to process requests on the served synapse. - The nucleus uses a prioritized thread pool to process requests according in priority. Priority is set by the Axon. - """ - - def __init__(self, config): - r""" Initializes a nucleus backward and forward threading pools. - """ - self._config = config - self._forward_pool = ThreadPoolExecutor(maxsize = self._config.nucleus.queue_maxsize, max_workers=self._config.nucleus.max_workers) - self._backward_pool = ThreadPoolExecutor(maxsize = self._config.nucleus.queue_maxsize, max_workers=self._config.nucleus.max_workers) - - def forward(self, synapse: Synapse, inputs: torch.Tensor, mode: bittensor_pb2.Modality, priority: float) -> Tuple[torch.FloatTensor, str, int]: - r""" Accepts a synapse object with inputs and priority, submits them to the forward work pool - and waits for a response from the threading future. Processing errors or timeouts result in - error codes which propagate back to the calling Axon. - - Args: - synapse (:obj:`bittensor.synapse.Synapse`, `required`): - synapse to pass to the worker. Note: the synapse.call_forward must be thread safe. - inputs (:obj:`torch.Tensor`, `required`): - tensor inputs to be passed to synapse.call_forward - mode (:enum:`bittensor_pb2.Modality`, `required`): - input modality enum signaling between IMAGE, TEXT or TENSOR inputs. - priority (`float`, `required`): - processing priority, a unique number from amongst current calls and less than sys.maxsize. - calls are processed in this order. - NOTE: priority must be unique amongst current calls. - Returns: - outputs (:obj:`torch.FloatTensor`, `required`): - response from the synapse.call_forward call or None in the case of errors. - message: (str, `required`): - message associated with forward call, potentially error, or 'success'. - code: (:obj:`bittensor_pb2.ReturnCode, `required`) - return code associated with forward call i.e. Success of Timeout. - """ - # Build future request. - call_params = [synapse, inputs, mode] - try: - future = self._forward_pool.submit( fn = self._forward, call_params = call_params, priority = priority) - except queue.Full: - code = bittensor_pb2.ReturnCode.NucleusFull - message = 'processing queue is full. try Backoff.' - return None, message, code - except Exception as e: - message = 'Unknown error on nucleus submit with error {}'.format(e) - logger.error(message) - code = bittensor_pb2.ReturnCode.UnknownException - return None, message, code - - # Try to get response or error on timeout. - try: - outputs = future.result (timeout = self._config.nucleus.queue_timeout) - tensor = outputs[0] - message = outputs[1] - code = outputs[2] - # Catch all Exception. - # Errors in the synapse call are caught in _backward and returned with the corresponding code. - except Exception as e: - tensor = None - message = 'timeout with error {}'.format(e) - code = bittensor_pb2.ReturnCode.NucleusTimeout - return tensor, message, code - - def backward(self, synapse: Synapse, inputs_x: torch.Tensor, grads_dy: torch.FloatTensor, mode: bittensor_pb2.Modality, priority: float) -> Tuple[torch.FloatTensor, str, int]: - r""" Accepts a synapse object with tensor inputs, grad inputs, and priority. - Submits inputs to the backward work pool for processing and waits waits for a response. - Processing errors or timeouts result in error codes which propagate back to the calling Axon. - - Args: - synapse (:obj:`bittensor.synapse.Synapse`, `required`): - synapse to pass to the worker. Note: the synapse.call_backward must be thread safe. - inputs_x (:obj:`torch.Tensor`, `required`): - tensor inputs from a previous call to be passed to synapse.call_backward - grads_dy (:obj:`torch.Tensor`, `required`): - gradients associated wiht inputs for backward call. - mode (:enum:`bittensor_pb2.Modality`, `required`): - input modality enum signaling between IMAGE, TEXT or TENSOR inputs. - priority (`float`, `required`): - processing priority, a unique number from amongst current calls and less than sys.maxsize. - calls are processed in this order. - NOTE: priority must be unique amongst current calls. - Returns: - outputs (:obj:`torch.FloatTensor`, `required`): - response from the synapse.call_backward call (i.e. inputs_dx) or None in the case of errors. - message: (str, `required`): - message associated with forward call, potentially error, or 'success'. - code: (:obj:`bittensor_pb2.ReturnCode, `required`) - return code associated with forward call i.e. Success of Timeout. - """ - # Build call params and submit the task to the pool. - call_params = [synapse, inputs_x, grads_dy, mode] - future = self._backward_pool.submit( fn = self._backward, call_params = call_params, priority = priority ) - # Recieve respoonse from the future or fail. - try: - outputs = future.result (timeout = self._config.nucleus.queue_timeout) - tensor = outputs[0] - message = outputs[1] - code = outputs[2] - # Catch all exception which returns a timeout code. - # Errors in the synapse call are caught in _backward and returned with the corresponding code. - except Exception as e: - tensor = None - message = 'timeout with error {}'.format(e) - code = bittensor_pb2.ReturnCode.NucleusTimeout - return tensor, message, code - - def _forward(self, call_params: List) -> Tuple[torch.FloatTensor, str, int]: - r""" Actual processing function for the forward call. The passed synapse.call_forward must be thread safe. - - Args: - call_params (:obj:`List[bittensor.synapse.Synapse, inputs, mode]`, `required`): - call params containing the synapse to be called and inputs with modality. - Returns: - outputs (:obj:`torch.FloatTensor`, `required`): - response from the synapse.call_backward call (i.e. inputs_dx) or None in the case of errors. - message: (str, `required`): - message associated with forward call, potentially error, or 'success'. - code: (:obj:`bittensor_pb2.ReturnCode, `required`) - return code associated with forward call i.e. Success of Timeout. - """ - synapse = call_params[0] - inputs = call_params[1] - mode = call_params[2] - try: - tensor = synapse.call_forward(inputs, mode) - message = 'success' - code = bittensor_pb2.ReturnCode.Success - - except NotImplementedError: - tensor = None - message = 'modality not implemented' - code = bittensor_pb2.ReturnCode.NotImplemented - - except Exception as e: - tensor = None - message = 'Unknown error when calling Synapse forward with errr {}'.format(e) - code = bittensor_pb2.ReturnCode.UnknownException - - return [tensor, message, code] - - def _backward(self, call_params: List): - r""" Actual processing function for the backward call. The passed synapse.call_backward must be thread safe. - - Args: - call_params (:obj:`List[bittensor.synapse.Synapse, inputs_x, grads_dy]`, `required`): - call params containing the synapse to be called and inputs with grads. - Returns: - outputs (:obj:`torch.FloatTensor`, `required`): - response from the synapse.call_backward call (i.e. inputs_dx) or None in the case of errors. - message: (str, `required`): - message associated with forward call, potentially error, or 'success'. - code: (:obj:`bittensor_pb2.ReturnCode, `required`) - return code associated with forward call i.e. Success of Timeout. - """ - - synapse = call_params[0] - inputs_x = call_params[1] - grads_dy = call_params[2] - mode = call_params[3] - try: - tensor = synapse.grad(inputs_x, grads_dy, modality = mode) - message = 'success' - code = bittensor_pb2.ReturnCode.Success - except Exception as e: - tensor = None - message = 'Unknown error when calling Synapse backward with errr {}'.format(e) - code = bittensor_pb2.ReturnCode.UnknownException - return [tensor, message, code] - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - r""" Adds this nucleus's command line arguments to the passed parser. - Args: - parser (:obj:`argparse.ArgumentParser`, `required`): - parser argument to append args to. - """ - parser.add_argument('--nucleus.max_workers', default=5, type=int, help='Nucleus priority queue workers.') - parser.add_argument('--nucleus.queue_timeout', default=5, type=int, help='Nucleus future timeout.') - parser.add_argument('--nucleus.queue_maxsize', default=1000, type=int, help='Maximum number of pending tasks allowed in the threading priority queue.') - - @staticmethod - def check_config(config: Munch): - r""" Checks the passed config items for validity. - Args: - config (:obj:`munch.Munch, `required`): - config to check. - """ - pass - - def __del__(self): - """ Calls nucleus stop for clean threadpool closure """ - self.stop() - - def stop(self): - """ Safely shutsdown the forward and backward pool thread """ - self._forward_pool.shutdown() - self._backward_pool.shutdown() - - diff --git a/bittensor/serializer.py b/bittensor/serializer.py deleted file mode 100644 index 790c00463c..0000000000 --- a/bittensor/serializer.py +++ /dev/null @@ -1,259 +0,0 @@ -""" An interface for serializing and deserializing bittensor tensors""" -import numpy as np -import torch - -import bittensor -from bittensor import bittensor_pb2 - -class SerializationException (Exception): - """ Raised during serialization """ - pass - -class DeserializationException (Exception): - """ Raised during deserialization """ - pass - -def torch_dtype_to_bittensor_dtype(tdtype): - """ Translates between torch.dtypes and bittensor.dtypes. - - Args: - tdtype (torch.dtype): torch.dtype to translate. - - Returns: - dtype: (bittensor.dtype): translated bittensor.dtype. - """ - if tdtype == torch.float32: - dtype = bittensor_pb2.DataType.FLOAT32 - elif tdtype == torch.float64: - dtype = bittensor_pb2.DataType.FLOAT64 - elif tdtype == torch.int32: - dtype = bittensor_pb2.DataType.INT32 - elif tdtype == torch.int64: - dtype = bittensor_pb2.DataType.INT64 - else: - dtype = bittensor_pb2.DataType.UNKNOWN - return dtype - - -def bittensor_dtype_to_torch_dtype(bdtype): - """ Translates between bittensor.dtype and torch.dtypes. - - Args: - bdtype (bittensor.dtype): bittensor.dtype to translate. - - Returns: - dtype: (torch.dtype): translated torch.dtype. - """ - if bdtype == bittensor_pb2.DataType.FLOAT32: - dtype = torch.float32 - elif bdtype == bittensor_pb2.DataType.FLOAT64: - dtype = torch.float64 - elif bdtype == bittensor_pb2.DataType.INT32: - dtype = torch.int32 - elif bdtype == bittensor_pb2.DataType.INT64: - dtype = torch.int64 - else: - raise DeserializationException( - 'Unknown bittensor.Dtype or no equivalent torch.dtype for bittensor.dtype = {}' - .format(bdtype)) - return dtype - - -def bittensor_dtype_np_dtype(bdtype): - """ Translates between bittensor.dtype and np.dtypes. - - Args: - bdtype (bittensor.dtype): bittensor.dtype to translate. - - Returns: - dtype: (numpy.dtype): translated np.dtype. - """ - if bdtype == bittensor_pb2.DataType.FLOAT32: - dtype = np.float32 - elif bdtype == bittensor_pb2.DataType.FLOAT64: - dtype = np.float64 - elif bdtype == bittensor_pb2.DataType.INT32: - dtype = np.int32 - elif bdtype == bittensor_pb2.DataType.INT64: - dtype = np.int64 - else: - raise SerializationException( - 'Unknown bittensor.dtype or no equivalent numpy.dtype for bittensor.dtype = {}' - .format(bdtype)) - return dtype - - -class PyTorchSerializer(): - - @staticmethod - def deserialize(proto: bittensor_pb2.Tensor) -> object: - """Deserializes an bittensor_pb2.Tensor to a torch.Tensor object. - - Args: - proto (bittensor_pb2.Tensor): Proto to derserialize. - - Returns: - List[object]: - """ - # Check typing - if proto.modality == bittensor_pb2.Modality.IMAGE: - return PyTorchSerializer.deserialize_image(proto) - - elif proto.modality == bittensor_pb2.Modality.TEXT: - return PyTorchSerializer.deserialize_text(proto) - - elif proto.modality == bittensor_pb2.Modality.TENSOR: - return PyTorchSerializer.deserialize_tensor(proto) - - else: - raise DeserializationException("Deserialization modality {} not implemented.".format(proto.modality)) - - @staticmethod - def serialize(tensor: torch.Tensor, - modality: bittensor_pb2.Modality) -> bittensor_pb2.Tensor: - """Serializes a object with modality into a bittensor Tensor proto. - - Args: - tensor (torch.Tensor): torch.Tensor object with modality TEXT, IMAGE, TENSOR - - Returns: - bittensor_pb2.Tensor: Serialized tensor as bittensor_pb2.proto. - """ - # Check typing - if modality == bittensor_pb2.Modality.IMAGE: - return PyTorchSerializer.serialize_image(tensor) - - elif modality == bittensor_pb2.Modality.TEXT: - return PyTorchSerializer.serialize_text(tensor) - - elif modality == bittensor_pb2.Modality.TENSOR: - return PyTorchSerializer.serialize_tensor(tensor) - - else: - raise SerializationException("Serialization modality {} not implemented.".format(modality)) - - @staticmethod - def serialize_text(tensor: torch.Tensor) -> bittensor_pb2.Tensor: - """Serializes a torch.Tensor to an bittensor Tensor proto. - - Args: - tensor (torch.Tensor): Tensor of tokenized string sequences. - - Returns: - bittensor_pb2.Tensor: Serialized tensor as bittensor_pb2.proto. - """ - if tensor.requires_grad: - data_buffer = tensor.cpu().detach().numpy().tobytes() - else: - data_buffer = tensor.cpu().numpy().tobytes() - - dtype = torch_dtype_to_bittensor_dtype(tensor.dtype) - shape = list(tensor.shape) - proto = bittensor_pb2.Tensor(version=bittensor.__version__, - buffer=data_buffer, - shape=shape, - dtype=dtype, - modality=bittensor_pb2.Modality.TEXT, - requires_grad=tensor.requires_grad) - return proto - - @staticmethod - def serialize_image(tensor: torch.Tensor) -> bittensor_pb2.Tensor: - """Serializes a torch.Tensor image into a bittensor Tensor proto. - - Args: - tensor (torch.Tensor): torch.Tensor of images to serialize. - - Returns: - bittensor_pb2.Tensor: Serialized tensor as bittensor_pb2.proto. - """ - if tensor.requires_grad: - data_buffer = tensor.cpu().detach().numpy().tobytes() - else: - data_buffer = tensor.cpu().numpy().tobytes() - - dtype = torch_dtype_to_bittensor_dtype(tensor.dtype) - shape = list(tensor.shape) - proto = bittensor_pb2.Tensor(version=bittensor.__version__, - buffer=data_buffer, - shape=shape, - dtype=dtype, - modality=bittensor_pb2.Modality.IMAGE, - requires_grad=tensor.requires_grad) - return proto - - @staticmethod - def serialize_tensor(tensor: torch.Tensor) -> bittensor_pb2.Tensor: - """Serializes a torch.Tensor to an bittensor Tensor proto. - - Args: - tensor (torch.Tensor): torch.Tensor to serialize. - - Returns: - bittensor_pb2.Tensor: Serialized tensor as bittensor_pb2.proto. - """ - if tensor.requires_grad: - data_buffer = tensor.cpu().detach().numpy().tobytes() - else: - data_buffer = tensor.cpu().numpy().tobytes() - - dtype = torch_dtype_to_bittensor_dtype(tensor.dtype) - shape = list(tensor.shape) - proto = bittensor_pb2.Tensor(version=bittensor.__version__, - buffer=data_buffer, - shape=shape, - dtype=dtype, - modality=bittensor_pb2.Modality.TENSOR, - requires_grad=tensor.requires_grad) - return proto - - @staticmethod - def deserialize_image(proto: bittensor_pb2.Tensor) -> torch.Tensor: - """Deserializes an bittensor_pb2.Tensor to a torch.Tensor object. - - Args: - proto (bittensor_pb2.Tensor): Proto to derserialize. - - Returns: - torch.Tensor: deserialized image tensor. - """ - dtype = bittensor_dtype_np_dtype(proto.dtype) - array = np.frombuffer(proto.buffer, dtype=np.dtype(dtype)).copy() - shape = tuple(proto.shape) - tensor = torch.as_tensor(array).view(shape).requires_grad_( - proto.requires_grad) - return tensor - - @staticmethod - def deserialize_text(proto: bittensor_pb2.Tensor) -> torch.Tensor: - """Deserializes an bittensor_pb2.Tensor to a torch.Tensor object. - - Args: - proto (bittensor_pb2.Tensor): Proto to derserialize. - - Returns: - torch.Tensor: deserialized text tensor. - """ - dtype = bittensor_dtype_np_dtype(proto.dtype) - array = np.frombuffer(proto.buffer, dtype=np.dtype(dtype)).copy() - shape = tuple(proto.shape) - tensor = torch.as_tensor(array).view(shape).requires_grad_( - proto.requires_grad) - return tensor - - @staticmethod - def deserialize_tensor(proto: bittensor_pb2.Tensor) -> torch.Tensor: - """Deserializes an bittensor_pb2.Tensor to a torch.Tensor object. - - Args: - proto (bittensor_pb2.Tensor): Proto to derserialize. - - Returns: - torch.Tensor: deserialized tensor. - """ - dtype = bittensor_dtype_np_dtype(proto.dtype) - array = np.frombuffer(proto.buffer, dtype=np.dtype(dtype)).copy() - shape = tuple(proto.shape) - tensor = torch.as_tensor(array).view(shape).requires_grad_( - proto.requires_grad) - return tensor diff --git a/bittensor/session.py b/bittensor/session.py deleted file mode 100644 index 936f9fb7b7..0000000000 --- a/bittensor/session.py +++ /dev/null @@ -1,260 +0,0 @@ -import argparse -from io import StringIO -import traceback as tb -from bittensor.utils.replicate_utils import ReplicateUtility -from munch import Munch - -from bittensor.synapse import Synapse -from bittensor.dendrite import Dendrite -from bittensor.axon import Axon -from bittensor.metagraph import Metagraph -from bittensor.nucleus import Nucleus -from bittensor.utils.asyncio import Asyncio -from bittensor.subtensor.interface import Keypair -from bittensor.exceptions.handlers import rollbar -from loguru import logger -from bittensor.crypto import is_encrypted, decrypt_data -from bittensor.utils import Cli -from bittensor.crypto import decrypt_keypair -from cryptography.exceptions import InvalidSignature, InvalidKey -from cryptography.fernet import InvalidToken -from bittensor.crypto.keyfiles import KeyFileError, load_keypair_from_data - -import json -import re -import stat - - -import os - -class FailedConnectToChain(Exception): - pass - -class FailedSubscribeToChain(Exception): - pass - -class FailedToEnterSession(Exception): - pass - -class FailedToPollChain(Exception): - pass - - - -class Session: - def __init__(self, config): - self.config = config - self.metagraph = Metagraph(self.config) - self.nucleus = Nucleus(self.config) - self.axon = Axon(self.config, self.nucleus) - self.dendrite = Dendrite(self.config) - - @staticmethod - def add_args(parser: argparse.ArgumentParser) -> argparse.ArgumentParser: - parser.add_argument('--session.hotkeyfile', required=False, default='~/.bittensor/hotkey', help="Path to the bittensor hot key file") - parser.add_argument('--session.coldkeyfile', required=False, default='~/.bittensor/coldkey', help="Path to the bittensor cold key file") - return parser - - @staticmethod - def check_config(config: Munch) -> Munch: - Session.__check_hot_key_path(config.session.hotkeyfile) - Session.__check_cold_key_path(config.session.coldkeyfile) - - @staticmethod - def __check_hot_key_path(path): - path = os.path.expanduser(path) - - if not Session.__has_keypair(path): - logger.info("No key found, generating a new one and storing it in {}", path) - keypair = Session.__create_keypair() - Session.__save_keypair(keypair, path) - - if not os.path.isfile(path): - logger.error("--session.hotkeyfile {} is not a file", path) - raise KeyFileError - - if not os.access(path, os.R_OK): - logger.error("--session.hotkeyfile {} is not readable", path) - raise KeyFileError - - if Session.__is_world_readable(path): - logger.error("--session.hotkeyfile {} is world readable.", path) - raise KeyFileError - - @staticmethod - def __is_world_readable(path): - st = os.stat(path) - return st.st_mode & stat.S_IROTH - - @staticmethod - def __check_cold_key_path(path): - path = os.path.expanduser(path) - - if not os.path.isfile(path): - logger.error("--session.coldkeyfile {} does not exist", path) - raise KeyFileError - - if not os.path.isfile(path): - logger.error("--session.coldkeyfile {} is not a file", path) - raise KeyFileError - - if not os.access(path, os.R_OK): - logger.error("--session.coldkeyfile {} is not readable", path) - raise KeyFileError - - with open(path, "r") as file: - key = file.readline().strip() - if not re.match("^0x[a-z0-9]{64}$", key): - logger.error("Cold key file corrupt") - raise KeyFileError - - @staticmethod - def __create_keypair() -> Keypair: - return Keypair.create_from_mnemonic(Keypair.generate_mnemonic()) - - @staticmethod - def __save_keypair(keypair : Keypair, path : str): - path = os.path.expanduser(path) - with open(path, 'w') as file: - json.dump(keypair.toDict(), file) - file.close() - - os.chmod(path, stat.S_IWUSR | stat.S_IRUSR) - - @staticmethod - def __has_keypair(path): - path = os.path.expanduser(path) - - return os.path.exists(path) - - @staticmethod - def load_cold_key(config): - path = config.session.coldkeyfile - path = os.path.expanduser(path) - logger.debug("Loading cold key from {}", path) - - with open(path, "r") as file: - config.session.coldkey = file.readline().strip() - - logger.info("Using coldkey : {}", config.session.coldkey) - - @staticmethod - def load_hotkeypair(config): - logger.info("Loading hot keypair") - keyfile = os.path.expanduser(config.session.hotkeyfile) - with open(keyfile, 'rb') as file: - data = file.read() - - if is_encrypted(data): - password = Cli.ask_password() - data = decrypt_data(password, data) - - hotkey = load_keypair_from_data(data) - config.session.keypair = hotkey - - - def __del__(self): - self.stop() - - def serve(self, synapse: Synapse): - r""" Serves a Synapse to the axon server replacing the previous Synapse if exists. - - Args: - synapse (:obj:`bittensor.Synapse`, `required`): - synapse object to serve on the axon server. - """ - self.axon.serve(synapse) - - def __enter__(self): - rollbar.init() # If a rollbar token is present, this will enable error reporting to rollbar - - logger.trace('session enter') - self.start() - return self - - def __exit__(self, exc_type, exc_value, exc_traceback): - """ Defines the exit protocol from asyncio task. - - Args: - exc_type (Type): The type of the exception. - exc_value (RuntimeError): The value of the exception, typically RuntimeError. - exc_traceback (traceback): The traceback that can be printed for this exception, detailing where error actually happend. - - Returns: - Session: present instance of session. - """ - self.stop() - if exc_value: - - top_stack = StringIO() - tb.print_stack(file=top_stack) - top_lines = top_stack.getvalue().strip('\n').split('\n')[:-4] - top_stack.close() - - full_stack = StringIO() - full_stack.write('Traceback (most recent call last):\n') - full_stack.write('\n'.join(top_lines)) - full_stack.write('\n') - tb.print_tb(exc_traceback, file=full_stack) - full_stack.write('{}: {}'.format(exc_type.__name__, str(exc_value))) - sinfo = full_stack.getvalue() - full_stack.close() - # Log the combined stack - logger.error('Exception:{}'.format(sinfo)) - - if rollbar.is_enabled(): - rollbar.send_exception() - - return self - - def start(self): - # Stop background grpc threads for serving the synapse object. - logger.info('Start axon server...') - try: - self.axon.start() - except Exception as e: - logger.error('SESSION: Failed to start axon server with error: {}', e) - raise FailedToEnterSession - - logger.trace('Connect to chain ...') - try: - connected = self.metagraph.connect() - if not connected: - logger.error('SESSION: Timeout while subscribing to the chain endpoint') - raise FailedConnectToChain - except Exception as e: - logger.error('SESSION: Error while connecting to the chain endpoint: {}', e) - raise FailedToEnterSession - - if - logger.info('Subscribe to chain ...') - try: - code, message = self.metagraph.subscribe(12) - if code != Metagraph.SubscribeSuccess: - logger.error('SESSION: Error while subscribing to the chain endpoint with message: {}', message) - raise FailedToEnterSession - - except Exception as e: - logger.error('SESSION: Error while subscribing to the chain endpoint: {}', e) - raise FailedToEnterSession - - def stop(self): - - logger.info('Shutting down the Axon server ...') - try: - self.axon.stop() - except Exception as e: - logger.error('SESSION: Error while stopping axon server: {} ', e) - - # Stop background grpc threads for serving synapse objects. - logger.info('Unsubscribe from chain ...') - try: - self.metagraph.unsubscribe() - except Exception as e: - logger.error('SESSION: Error while unsubscribing to the chain endpoint: {}', e) - - def subscribe (self): - self.metagraph.subscribe() - - def unsubscribe (self): - self.metagraph.unsubscribe() diff --git a/bittensor/subtensor/__pycache__/__init__.cpython-38.pyc b/bittensor/subtensor/__pycache__/__init__.cpython-38.pyc deleted file mode 100644 index 164485b63d..0000000000 Binary files a/bittensor/subtensor/__pycache__/__init__.cpython-38.pyc and /dev/null differ diff --git a/bittensor/subtensor/client/__init__.py b/bittensor/subtensor/client/__init__.py deleted file mode 100644 index f71fe47ec2..0000000000 --- a/bittensor/subtensor/client/__init__.py +++ /dev/null @@ -1,218 +0,0 @@ -from bittensor.subtensor.interface import SubstrateWSInterface, Keypair -import netaddr -from loguru import logger -from bittensor.balance import Balance -from .neurons import Neuron, Neurons - -class WSClient: - custom_type_registry = { - "runtime_id": 2, - "types": { - "NeuronMetadataOf": { - "type": "struct", - "type_mapping": [["ip", "u128"], ["port", "u16"], ["ip_type", "u8"], ["uid", "u64"], ["coldkey", "AccountId"]] - } - } - } - - - def __init__(self, socket : str, keypair: Keypair): - host, port = socket.split(":") - - self.substrate = SubstrateWSInterface( - host=host, - port=int(port), - address_type = 42, - type_registry_preset='substrate-node-template', - type_registry=self.custom_type_registry, - ) - - self.__keypair = keypair - - ''' - PRIVATE METHODS - ''' - - def __int_to_ip(self, int_val): - return str(netaddr.IPAddress(int_val)) - - def __ip_to_int(self, str_val): - return int(netaddr.IPAddress(str_val)) - - - ''' - PUBLIC METHODS - ''' - - def connect(self): - logger.trace("connect() C") - self.substrate.connect() - - def is_connected(self): - return self.substrate.is_connected() - - async def subscribe(self, ip: str, port: int, coldkey: str): - params = { - 'ip': self.__ip_to_int(ip), - 'port': port, - 'ip_type': 4, - 'coldkey': coldkey - } - - call = await self.substrate.compose_call( - call_module='SubtensorModule', - call_function='subscribe', - call_params=params - ) - - extrinsic = await self.substrate.create_signed_extrinsic(call=call, keypair=self.__keypair) - await self.substrate.submit_extrinsic(extrinsic, wait_for_inclusion=False) # Waiting for inclusion and other does not work - - async def unsubscribe(self, keypair=None): - if not keypair: - keypair = self.__keypair - - call = await self.substrate.compose_call( - call_module='SubtensorModule', - call_function='unsubscribe' - ) - - extrinsic = await self.substrate.create_signed_extrinsic(call=call, keypair=keypair) - await self.substrate.submit_extrinsic(extrinsic, wait_for_inclusion=False) - - # TODO (21-12-2020, Parall4x) Delete if not needed anymore - # async def get_peers(self): - # peers = await self.substrate.iterate_map( - # module='SubtensorModule', - # storage_function='Peers') - # - # return peers - - async def get_balance(self, address): - logger.debug("Getting balance for: {}", address) - result = await self.substrate.get_runtime_state( - module='System', - storage_function='Account', - params=[address], - block_hash=None - ) - - balance_info = result.get('result') - if not balance_info: - logger.debug("{} has no balance", address) - return Balance(0) - - balance = balance_info['data']['free'] - logger.debug("{} has {} rao", address, balance) - return Balance(balance) - - async def add_stake(self, amount : Balance, hotkey_id): - logger.debug("Adding stake of {} rao from coldkey {} to hotkey {}", amount.rao, self.__keypair.public_key, hotkey_id) - call = await self.substrate.compose_call( - call_module='SubtensorModule', - call_function='add_stake', - call_params={ - 'hotkey': hotkey_id, - 'ammount_staked': amount.rao - } - ) - - extrinsic = await self.substrate.create_signed_extrinsic(call=call, keypair=self.__keypair) - await self.substrate.submit_extrinsic(extrinsic, wait_for_inclusion=False) - - async def unstake(self, amount : Balance, hotkey_id): - logger.debug("Requesting unstake of {} rao for hotkey: {} to coldkey: {}", amount.rao, hotkey_id, self.__keypair.public_key) - call = await self.substrate.compose_call( - call_module='SubtensorModule', - call_function='remove_stake', - call_params={'ammount_unstaked': amount.rao, 'hotkey': hotkey_id} - ) - - extrinsic = await self.substrate.create_signed_extrinsic(call=call, keypair=self.__keypair) - await self.substrate.submit_extrinsic(extrinsic, wait_for_inclusion=False) - - async def set_weights(self, destinations, values, keypair, wait_for_inclusion=False): - call = await self.substrate.compose_call( - call_module = 'SubtensorModule', - call_function = 'set_weights', - call_params = {'dests': destinations, 'weights': values} - ) - - extrinsic = await self.substrate.create_signed_extrinsic(call=call, keypair=keypair) - await self.substrate.submit_extrinsic(extrinsic, wait_for_inclusion=wait_for_inclusion) - - async def emit(self, destinations, values, keypair, wait_for_inclusion=False): - call = await self.substrate.compose_call( - call_module='SubtensorModule', - call_function='set_weights', - call_params = {'dests': destinations, 'weights': values} - ) - extrinsic = await self.substrate.create_signed_extrinsic(call=call, keypair=keypair) - await self.substrate.submit_extrinsic(extrinsic, wait_for_inclusion=wait_for_inclusion) - - async def get_current_block(self): - return await self.substrate.get_block_number(None) - - async def neurons(self, pubkey=None, decorator=False): - - # Todo (parall4x, 23-12-2020) Get rid of this decorator flag. This should be refactored into something that returns Neuron objects only - if pubkey: - result = await self.substrate.get_runtime_state( - module='SubtensorModule', - storage_function='Neurons', - params=[pubkey] - ) - return Neurons.from_list(result['result']) if decorator else result['result'] - - else: - neurons = await self.substrate.iterate_map( - module='SubtensorModule', - storage_function='Neurons' - ) - return Neurons.from_list(neurons) if decorator else neurons - - async def get_stake_for_uid(self, uid) -> Balance: - stake = await self.substrate.get_runtime_state( - module='SubtensorModule', - storage_function='Stake', - params = [uid] - ) - - if not stake: - return Balance(0) - - return Balance(stake['result']) - - async def weight_uids_for_uid(self, uid): - result = await self.substrate.get_runtime_state( - module='SubtensorModule', - storage_function='WeightUids', - params = [uid] - ) - return result['result'] - - async def weight_vals_for_uid(self, uid): - result = await self.substrate.get_runtime_state( - module='SubtensorModule', - storage_function='WeightVals', - params = [uid] - ) - return result['result'] - - async def get_last_emit_data_for_uid(self, uid): - result = await self.substrate.get_runtime_state( - module='SubtensorModule', - storage_function='LastEmit', - params = [uid] - ) - return result['result'] - - async def get_last_emit_data(self): - result = await self.substrate.iterate_map( - module='SubtensorModule', - storage_function='LastEmit' - ) - return result - - - diff --git a/bittensor/subtensor/client/__pycache__/__init__.cpython-38.pyc b/bittensor/subtensor/client/__pycache__/__init__.cpython-38.pyc deleted file mode 100644 index 3ce6f46209..0000000000 Binary files a/bittensor/subtensor/client/__pycache__/__init__.cpython-38.pyc and /dev/null differ diff --git a/bittensor/subtensor/client/__pycache__/ip.cpython-38.pyc b/bittensor/subtensor/client/__pycache__/ip.cpython-38.pyc deleted file mode 100644 index 963414f613..0000000000 Binary files a/bittensor/subtensor/client/__pycache__/ip.cpython-38.pyc and /dev/null differ diff --git a/bittensor/subtensor/client/__pycache__/neurons.cpython-38.pyc b/bittensor/subtensor/client/__pycache__/neurons.cpython-38.pyc deleted file mode 100644 index eacad00651..0000000000 Binary files a/bittensor/subtensor/client/__pycache__/neurons.cpython-38.pyc and /dev/null differ diff --git a/bittensor/subtensor/client/ip.py b/bittensor/subtensor/client/ip.py deleted file mode 100644 index 7ed9505b48..0000000000 --- a/bittensor/subtensor/client/ip.py +++ /dev/null @@ -1,21 +0,0 @@ -import socket -import struct - -class IP: - ip: int - ip_type: int - - def __init__(self, ip, ip_type): - self.ip = ip - self.ip_type = ip_type - - def __str__(self): - return "/ipv%i/%s" % (self.ip_type, IP.int2ip(self.ip)) - - @staticmethod - def ip2int(addr): - return struct.unpack("!I", socket.inet_aton(addr))[0] - - @staticmethod - def int2ip(addr): - return socket.inet_ntoa(struct.pack("!I", addr)) \ No newline at end of file diff --git a/bittensor/subtensor/client/neurons.py b/bittensor/subtensor/client/neurons.py deleted file mode 100644 index b7be39f252..0000000000 --- a/bittensor/subtensor/client/neurons.py +++ /dev/null @@ -1,62 +0,0 @@ -from .ip import IP - -from loguru import logger - - - -class Neuron: - uid: int - hotkey : str - ip: IP - coldkey : str - - def __init__(self, uid, hotkey, ip, ip_type, coldkey): - self.uid = uid - self.hotkey = hotkey - self.ip = IP(ip, ip_type) - self.coldkey = coldkey - self.stake = int - - @staticmethod - def from_dict(attrs : dict): - return Neuron(attrs['uid'], attrs['hotkey'], attrs['ip'], attrs['ip_type'], attrs['coldkey']) - - def __str__(self): - return "" % (self.uid, self.hotkey, self.ip, self.coldkey) - - -class Neurons(list): - @staticmethod - def from_list(input : list): - output = Neurons() - - if not input: - return output - - for row in input: - data = row[1] # Attributes of the neuron are stored in the second element of the list - data['hotkey'] = row[0] # the hotkey pub key is stored in the first element - output.append(Neuron.from_dict(data)) - - return output - - def has_uid(self,uid): - neurons = filter(lambda x: x.uid == uid, self) - return len(list(neurons)) > 0 - - def get_by_uid(self, uid): - neurons = Neurons(filter(lambda x: x.uid == uid, self)) - return None if len(neurons) == 0 else neurons[0] - - - - - def __str__(self): - y = map(lambda x : x.__str__(), self) - - return "".join(y) - - - - - diff --git a/bittensor/subtensor/interface/__init__.py b/bittensor/subtensor/interface/__init__.py deleted file mode 100644 index 08faa34733..0000000000 --- a/bittensor/subtensor/interface/__init__.py +++ /dev/null @@ -1,2058 +0,0 @@ -# Python Substrate Interface Library -# -# Copyright 2018-2020 Stichting Polkascan (Polkascan Foundation) on class KeyPairType, Keypair -# Copyright 2020 OpenTensor on remaining code -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import asyncio -from hashlib import blake2b -import binascii -import json, yaml -from loguru import logger -import re - -from autobahn.asyncio.websocket import WebSocketClientProtocol, WebSocketClientFactory - - -from scalecodec import ScaleBytes, GenericCall -from scalecodec.base import ScaleDecoder, RuntimeConfiguration -from scalecodec.block import ExtrinsicsDecoder, EventsDecoder, LogDigest -from scalecodec.metadata import MetadataDecoder -from scalecodec.type_registry import load_type_registry_preset -from scalecodec.updater import update_type_registries - -from .key import extract_derive_path -from .subkey import Subkey -from .utils.hasher import blake2_256, two_x64_concat, xxh128, blake2_128, blake2_128_concat, identity -from .exceptions import SubstrateRequestException, ConfigurationError, StorageFunctionNotFound -from .constants import * -from .utils.ss58 import ss58_decode, ss58_encode - -from bip39 import bip39_to_mini_secret, bip39_generate -import sr25519 -import ed25519 - - - - -class KeypairType: - ED25519 = 0 - SR25519 = 1 - - - - - - -class Keypair: - - def __init__(self, ss58_address=None, public_key=None, private_key=None, address_type=42, seed_hex=None, - crypto_type=KeypairType.SR25519): - - self.crypto_type = crypto_type - self.seed_hex = seed_hex - self.derive_path = None - - if ss58_address and not public_key: - public_key = ss58_decode(ss58_address) - - if not public_key: - raise ValueError('No SS58 formatted address or public key provided') - - if type(public_key) is bytes: - public_key = public_key.hex() - - public_key = '0x{}'.format(public_key.replace('0x', '')) - - if len(public_key) != 66: - raise ValueError('Public key should be 32 bytes long') - - if not ss58_address: - ss58_address = ss58_encode(public_key, address_type=address_type) - - self.public_key = public_key - self.ss58_address = ss58_address - - if private_key: - - if type(private_key) is bytes: - private_key = private_key.hex() - - private_key = '0x{}'.format(private_key.replace('0x', '')) - - if self.crypto_type == KeypairType.SR25519 and len(private_key) != 130: - raise ValueError('Secret key should be 64 bytes long') - - self.private_key = private_key - self.address_type = address_type - - self.mnemonic = None - - @classmethod - def generate_mnemonic(cls, words=12): - return bip39_generate(words) - - @classmethod - def create_from_mnemonic(cls, mnemonic, address_type=42, crypto_type=KeypairType.SR25519): - seed_array = bip39_to_mini_secret(mnemonic, "") - - keypair = cls.create_from_seed( - seed_hex=binascii.hexlify(bytearray(seed_array)).decode("ascii"), - address_type=address_type, - crypto_type=crypto_type - ) - keypair.mnemonic = mnemonic - - return keypair - - @classmethod - def create_from_seed(cls, seed_hex, address_type=42, crypto_type=KeypairType.SR25519): - - if crypto_type == KeypairType.SR25519: - public_key, private_key = sr25519.pair_from_seed(bytes.fromhex(seed_hex.replace('0x', ''))) - elif crypto_type == KeypairType.ED25519: - private_key, public_key = ed25519.ed_from_seed(bytes.fromhex(seed_hex.replace('0x', ''))) - else: - raise ValueError('crypto_type "{}" not supported'.format(crypto_type)) - - public_key = public_key.hex() - private_key = private_key.hex() - - ss58_address = ss58_encode(public_key, address_type) - - return cls( - ss58_address=ss58_address, public_key=public_key, private_key=private_key, - address_type=address_type, crypto_type=crypto_type, seed_hex=seed_hex - ) - - @classmethod - def create_from_uri(cls, suri, address_type=42, crypto_type=KeypairType.SR25519): - - if suri and suri.startswith('/'): - suri = DEV_PHRASE + suri - - suri_regex = re.match(r'^(?P\w+( \w+)*)(?P(//?[^/]+)*)(///(?P.*))?$', suri) - - suri_parts = suri_regex.groupdict() - - if suri_parts['password']: - raise NotImplementedError("Passwords in suri not supported") - - derived_keypair = cls.create_from_mnemonic( - suri_parts['phrase'], address_type=address_type, crypto_type=crypto_type - ) - - if suri_parts['path'] != '': - - derived_keypair.derive_path = suri_parts['path'] - - if crypto_type not in [KeypairType.SR25519]: - raise NotImplementedError('Derivation paths for this crypto type not supported') - - derive_junctions = extract_derive_path(suri_parts['path']) - - child_pubkey = bytes.fromhex(derived_keypair.public_key[2:]) - child_privkey = bytes.fromhex(derived_keypair.private_key[2:]) - - for junction in derive_junctions: - - if junction.is_hard: - - _, child_pubkey, child_privkey = sr25519.hard_derive_keypair( - (junction.chain_code, child_pubkey, child_privkey), - b'' - ) - - else: - - _, child_pubkey, child_privkey = sr25519.derive_keypair( - (junction.chain_code, child_pubkey, child_privkey), - b'' - ) - - derived_keypair = Keypair(public_key=child_pubkey, private_key=child_privkey) - - return derived_keypair - - @classmethod - def create_from_private_key( - cls, private_key, public_key=None, ss58_address=None, address_type=42, crypto_type=KeypairType.SR25519 - ): - return cls( - ss58_address=ss58_address, public_key=public_key, private_key=private_key, - address_type=address_type, crypto_type=crypto_type - ) - - def sign(self, data): - """ - Creates a sr25519 signature with give data - - Parameters - ---------- - data - - Returns - ------- - sr25519 signature - - """ - if type(data) is ScaleBytes: - data = bytes(data.data) - elif data[0:2] == '0x': - data = bytes.fromhex(data[2:]) - else: - data = data.encode() - - if not self.private_key: - raise ConfigurationError('No private key set to create signatures') - - if self.crypto_type == KeypairType.SR25519: - - signature = sr25519.sign((bytes.fromhex(self.public_key[2:]), bytes.fromhex(self.private_key[2:])), data) - elif self.crypto_type == KeypairType.ED25519: - signature = ed25519.ed_sign(bytes.fromhex(self.public_key[2:]), bytes.fromhex(self.private_key[2:]), data) - else: - raise ConfigurationError("Crypto type not supported") - - return "0x{}".format(signature.hex()) - - def verify(self, data, signature): - - if type(data) is ScaleBytes: - data = bytes(data.data) - elif data[0:2] == '0x': - data = bytes.fromhex(data[2:]) - else: - data = data.encode() - - if type(signature) is str and signature[0:2] == '0x': - signature = bytes.fromhex(signature[2:]) - - if type(signature) is not bytes: - raise TypeError("Signature should be of type bytes or a hex-string") - - if self.crypto_type == KeypairType.SR25519: - return sr25519.verify(signature, data, bytes.fromhex(self.public_key[2:])) - elif self.crypto_type == KeypairType.ED25519: - return ed25519.ed_verify(signature, data, bytes.fromhex(self.public_key[2:])) - else: - raise ConfigurationError("Crypto type not supported") - - def __repr__(self): - return '(ss58_address={})'.format(self.ss58_address) - - def toDict(self): - return { - 'accountId': self.public_key, - 'publicKey': self.public_key, - 'secretPhrase': self.mnemonic, - 'secretSeed': "0x" + self.seed_hex, - 'ss58Address': self.ss58_address - } - -def KeypairRepresenter(dumper, data): - serializedData = data.__repr__() - - logger.debug(serializedData) - return dumper.represent_scalar('Keypair', serializedData) - - -yaml.add_representer(Keypair, KeypairRepresenter) - - - - - - - - -class SubtensorClientProtocol(WebSocketClientProtocol): - - def __init__(self): - self.futures = dict() - self.msg_id = 0 - WebSocketClientProtocol.__init__(self) - - def get_future(self, id): - if not id in self.futures: - return None - - return self.futures[id] - - def onConnecting(self, transport_details): - logger.trace("Connecting to websocket server {}", transport_details) - self.factory.connection = self - - def onConnect(self, response): - logger.trace("Connected. {}", response) - - def onOpen(self): - logger.trace("Connection open to websocket established") - self.factory.connected.set_result(True) - - def onMessage(self, payload, isBinary): - logger.trace("Message received: {}", payload) - - str_payload = json.loads(payload) - id = str_payload['id'] - if id in self.futures: - self.futures[id].set_result(str_payload) - del self.futures[id] - else: - logger.debug("Deferred with id {} not in list", id) - - def onClose(self, wasClean, code, reason): - logger.trace("Connection closed.") - #Todo implement - - def sendMessage(self, payload, isBinary=False, fragmentSize=None, sync=False, doNotCompress=False): - logger.trace("Sending message {}", payload) - - super().sendMessage(payload, isBinary, fragmentSize, sync, doNotCompress) - - def asyncSendMessage(self, payload): - self.sendMessage(payload) - loop = asyncio.get_event_loop() - future = loop.create_future() - - self.futures[self.msg_id] = future - - return future - - def async_rpc_request(self, method, params): - self.msg_id += 1 - payload = { - "jsonrpc": "2.0", - "method": method, - "params": params, - "id": self.msg_id - } - - return self.asyncSendMessage(json.dumps(payload).encode('utf8')) - - - - - -class SubTensorWebSocketClientFactory(WebSocketClientFactory): - def __init__(self, url): - self.protocol = SubtensorClientProtocol - self.connection = None - - WebSocketClientFactory.__init__(self, url) - - loop = asyncio.get_event_loop() - self.connected = loop.create_future() - - def is_connected(self): - return self.connected - - - - - -class SubstrateWSInterface: - def __init__(self, host, port, address_type=None, type_registry=None, type_registry_preset="default", cache_region=None, sub_key: Subkey = None): - """ - A specialized class in interfacing with a Substrate node. - - Parameters - ---------- - url: the URL to the substrate node, either in format https://127.0.0.1:9933 or wss://127.0.0.1:9944 - address_type: The address type which account IDs will be SS58-encoded to Substrate addresses. Defaults to 42, for Kusama the address type is 2 - type_registry: A dict containing the custom type registry in format: {'types': {'customType': 'u32'},..} - type_registry_preset: The name of the predefined type registry shipped with the SCALE-codec, e.g. kusama - cache_region: a Dogpile cache region as a central store for the metadata cache - """ - - self.host = host - self.port = port - self.factory = None - - self.cache_region = cache_region - - if type_registry_preset: - # Load type registries in runtime configuration - RuntimeConfiguration().update_type_registry(load_type_registry_preset("default")) - - if type_registry != "default": - RuntimeConfiguration().update_type_registry(load_type_registry_preset(type_registry_preset)) - - if type_registry: - # Load type registries in runtime configuration - RuntimeConfiguration().update_type_registry(type_registry) - - self.address_type = address_type - - self.mock_extrinsics = None - self._version = None - self.default_headers = { - 'content-type': "application/json", - 'cache-control': "no-cache" - } - - self.metadata_decoder = None - - self.runtime_version = None - self.transaction_version = None - - self.block_hash = None - self.block_id = None - - self.metadata_cache = {} - self.type_registry_cache = {} - - self.sub_key = sub_key - - self.debug = False - - # Map from pubkey to nonce - self.nonce = {} - - def connect(self): - self.factory = SubTensorWebSocketClientFactory("ws://%s:%i" % (self.host, self.port)) - - loop = asyncio.get_event_loop() - coro = loop.create_connection(self.factory, self.host, self.port) - loop.create_task(coro) - - def is_connected(self): - if not self.factory: - raise Exception("connect() call not issued") - - return self.factory.is_connected() - - def send_message(self, message): - return self.factory.connection.sendMessage(message) - - def send_message_future(self, message): - return self.factory.connection.sendMessageWithFuture(message) - - - # ************* - # ASYNC METHODS - # ************* - - async def get_system_name(self): - """ - A pass-though to existing JSONRPC method `system_name` - - Returns - ------- - - """ - response = await self.factory.connection.async_rpc_request("system_name", []) - return response.get('result') - - async def get_version(self): - """ - A pass-though to existing JSONRPC method `system_version` - - Returns - ------- - - """ - if not self._version: - response = await self.factory.connection.async_rpc_request("system_version", []) - self._version = response.get('result') - return self._version - - async def get_chain_head(self): - """ - A pass-though to existing JSONRPC method `chain_getHead` - - Returns - ------- - - """ - return await self.factory.connection.async_rpc_request("chain_getHead", []) - - async def get_chain_finalised_head(self): - """ - A pass-though to existing JSONRPC method `chain_getFinalisedHead` - - Returns - ------- - - """ - response = await self.factory.connection.async_rpc_request("chain_getFinalisedHead", []) - return response.get('result') - - - async def get_chain_block(self, block_hash=None, block_id=None, metadata_decoder=None): - if block_id: - block_hash = self.get_block_hash(block_id) - - response = await self.factory.connection.async_rpc_request("chain_getBlock", [block_hash]).get('result') - - if self.mock_extrinsics: - # Extend extrinsics with mock_extrinsics for e.g. performance tests - response['block']['extrinsics'].extend(self.mock_extrinsics) - - # Decode extrinsics - if metadata_decoder: - - response['block']['header']['number'] = int(response['block']['header']['number'], 16) - - for idx, extrinsic_data in enumerate(response['block']['extrinsics']): - extrinsic_decoder = ExtrinsicsDecoder( - data=ScaleBytes(extrinsic_data), - metadata=metadata_decoder - ) - extrinsic_decoder.decode() - response['block']['extrinsics'][idx] = extrinsic_decoder.value - - for idx, log_data in enumerate(response['block']['header']["digest"]["logs"]): - log_digest = LogDigest(ScaleBytes(log_data)) - log_digest.decode() - response['block']['header']["digest"]["logs"][idx] = log_digest.value - - return response - - - async def get_block_hash(self, block_id): - response = await self.factory.connection.async_rpc_request("chain_getBlockHash", [block_id]) - return response.get('result') - - async def get_block_header(self, block_hash): - response = await self.factory.connection.async_rpc_request("chain_getHeader", [block_hash]) - return response.get('result') - - - async def get_block_number(self, block_hash): - response = await self.factory.connection.async_rpc_request("chain_getHeader", [block_hash]) - return int(response['result']['number'], 16) - - async def get_block_metadata(self, block_hash=None, decode=True): - """ - A pass-though to existing JSONRPC method `state_getMetadata`. For a decoded version see `get_runtime_metadata()` - - Parameters - ---------- - block_hash - decode: DEPRECATED use `get_runtime_metadata()` for decoded version - - Returns - ------- - - """ - params = None - if block_hash: - params = [block_hash] - response = await self.factory.connection.async_rpc_request("state_getMetadata", params) - - if decode: - metadata_decoder = MetadataDecoder(ScaleBytes(response.get('result'))) - metadata_decoder.decode() - - return metadata_decoder - - return response - - async def get_system_peers(self): - """ - Retrieves the peers connected to the node - """ - - return await self.factory.connection.async_rpc_request("system_peers", []) - - async def get_storage(self, block_hash, module, function, params=None, return_scale_type=None, hasher=None, - spec_version_id='default', metadata=None, metadata_version=None): - """ - Retrieves the storage entry for given module, function and optional parameters at given block. - - DEPRECATED: use `get_runtime_state()` - - Parameters - ---------- - block_hash - module - function - params - return_scale_type: Scale type string to interprete result - hasher: Hashing method used to determine storage key, defaults to 'Twox64Concat' if not provided - spec_version_id: DEPRECATED - metadata - metadata_version: Version index of Metadata, e.g. 9 for MetadataV9 - - Returns - ------- - - """ - storage_hash = self.generate_storage_hash( - storage_module=module, - storage_function=function, - params=params, - hasher=hasher, - metadata_version=metadata_version - ) - response = await self.factory.connection.async_rpc_request("state_getStorageAt", [storage_hash, block_hash]) - - if 'result' in response: - - if return_scale_type and response.get('result'): - obj = ScaleDecoder.get_decoder_class( - return_scale_type, - ScaleBytes(response.get('result')), - metadata=metadata - ) - return obj.decode() - else: - return response.get('result') - else: - raise SubstrateRequestException("Error occurred during retrieval of events") - - - async def get_storage_by_key(self, block_hash, storage_key): - """ - A pass-though to existing JSONRPC method `state_getStorageAt` - - Parameters - ---------- - block_hash - storage_key - - Returns - ------- - - """ - - response = await self.factory.connection.async_rpc_request("state_getStorageAt", [storage_key, block_hash]) - if 'result' in response: - return response.get('result') - else: - raise SubstrateRequestException("Error occurred during retrieval of events") - - async def get_block_events(self, block_hash, metadata_decoder=None): - """ - A convenience method to fetch the undecoded events from storage - - Parameters - ---------- - block_hash - metadata_decoder - - Returns - ------- - - """ - - if metadata_decoder and metadata_decoder.version.index >= 9: - storage_hash = STORAGE_HASH_SYSTEM_EVENTS_V9 - else: - storage_hash = STORAGE_HASH_SYSTEM_EVENTS - - response = await self.factory.connection.async_rpc_request("state_getStorageAt", [storage_hash, block_hash]) - - if response.get('result'): - - if metadata_decoder: - # Process events - events_decoder = EventsDecoder( - data=ScaleBytes(response.get('result')), - metadata=metadata_decoder - ) - events_decoder.decode() - - return events_decoder - - else: - return response - else: - raise SubstrateRequestException("Error occurred during retrieval of events") - - async def get_block_runtime_version(self, block_hash): - """ - Retrieve the runtime version id of given block_hash - Parameters - ---------- - block_hash - - Returns - ------- - - """ - response = await self.factory.connection.async_rpc_request("chain_getRuntimeVersion", [block_hash]) - return response.get('result') - - def generate_storage_hash(self, storage_module, storage_function, params=None, hasher=None, key2_hasher=None, - metadata_version=None): - """ - Generate a storage key for given module/function - - Parameters - ---------- - storage_module - storage_function - params: Parameters of the storage function, provided in scale encoded hex-bytes - hasher: Hashing method used to determine storage key, defaults to 'Twox64Concat' if not provided - metadata_version: Version index of Metadata, e.g. 9 for MetadataV9 - - Returns - ------- - - """ - - if not metadata_version or metadata_version >= 9: - storage_hash = xxh128(storage_module.encode()) + xxh128(storage_function.encode()) - - if params: - - if type(params) is not list: - params = [params] - - for idx, param in enumerate(params): - if idx == 0: - param_hasher = hasher - elif idx == 1: - param_hasher = key2_hasher - else: - raise ValueError('Unexpected third parameter for storage call') - - params_key = bytes() - - if type(param) is str: - params_key += binascii.unhexlify(param) - elif type(param) is ScaleBytes: - params_key += param.data - elif isinstance(param, ScaleDecoder): - params_key += param.data.data - - if not param_hasher: - param_hasher = 'Twox128' - - if param_hasher == 'Blake2_256': - storage_hash += blake2_256(params_key) - - elif param_hasher == 'Blake2_128': - storage_hash += blake2_128(params_key) - - elif param_hasher == 'Blake2_128Concat': - storage_hash += blake2_128_concat(params_key) - - elif param_hasher == 'Twox128': - storage_hash += xxh128(params_key) - - elif param_hasher == 'Twox64Concat': - storage_hash += two_x64_concat(params_key) - - elif param_hasher == 'Identity': - storage_hash += identity(params_key) - - else: - raise ValueError('Unknown storage hasher "{}"'.format(param_hasher)) - - return '0x{}'.format(storage_hash) - - else: - storage_hash = storage_module.encode() + b" " + storage_function.encode() - - if params: - storage_hash += binascii.unhexlify(params) - - # Determine hasher function - if not hasher: - hasher = 'Twox128' - - if hasher == 'Blake2_256': - return "0x{}".format(blake2_256(storage_hash)) - - elif hasher == 'Twox128': - return "0x{}".format(xxh128(storage_hash)) - - elif hasher == 'Twox64Concat': - return "0x{}".format(two_x64_concat(storage_hash)) - - def convert_storage_parameter(self, scale_type, value): - if scale_type == 'AccountId': - if value[0:2] != '0x': - return '0x{}'.format(ss58_decode(value, self.address_type)) - - return value - - # Runtime functions used by Substrate API - - async def init_runtime(self, block_hash=None, block_id=None): - """ - This method is used by all other methods that deals with metadata and types defined in the type registry. - It optionally retrieves the block_hash when block_id is given and sets the applicable metadata for that - block_hash. Also it applies all the versioned types at the time of the block_hash. - - Because parsing of metadata and type registry is quite heavy, the result will be cached per runtime id. - In the future there could be support for caching backends like Redis to make this cache more persistent. - - Parameters - ---------- - block_hash - block_id - - Returns - ------- - - """ - - if block_id and block_hash: - raise ValueError('Cannot provide block_hash and block_id at the same time') - - # Check if runtime state already set to current block - if (block_hash and block_hash == self.block_hash) or (block_id and block_id == self.block_id): - return - - if block_id is not None: - block_hash = self.get_block_hash(block_id) - - self.block_hash = block_hash - self.block_id = block_id - - runtime_info = await self.get_block_runtime_version(block_hash=self.block_hash) - - # Check if runtime state already set to current block - if runtime_info.get("specVersion") == self.runtime_version: - return - - self.runtime_version = runtime_info.get("specVersion") - self.transaction_version = runtime_info.get("transactionVersion") - - # Set active runtime version - RuntimeConfiguration().set_active_spec_version_id(self.runtime_version) - - if self.runtime_version not in self.metadata_cache and self.cache_region: - # Try to retrieve metadata from Dogpile cache - cached_metadata = self.cache_region.get('METADATA_{}'.format(self.runtime_version)) - if cached_metadata: - self.debug_message('Retrieved metadata for {} from Redis'.format(self.runtime_version)) - self.metadata_cache[self.runtime_version] = cached_metadata - - if self.runtime_version in self.metadata_cache: - # Get metadata from cache - self.debug_message('Retrieved metadata for {} from memory'.format(self.runtime_version)) - self.metadata_decoder = self.metadata_cache[self.runtime_version] - else: - self.metadata_decoder = await self.get_block_metadata(block_hash=self.block_hash, decode=True) - self.debug_message('Retrieved metadata for {} from Substrate node'.format(self.runtime_version)) - - # Update metadata cache - self.metadata_cache[self.runtime_version] = self.metadata_decoder - - if self.cache_region: - self.debug_message('Stored metadata for {} in Redis'.format(self.runtime_version)) - self.cache_region.set('METADATA_{}'.format(self.runtime_version), self.metadata_decoder) - - async def iterate_map(self, module, storage_function, block_hash=None): - """ - iterates over all key-pairs localted at the given module and storage_function. The storage - item must be a map. - - Parameters - ---------- - module: The module name in the metadata, e.g. Balances or Account. - storage_function: The storage function name, e.g. FreeBalance or AccountNonce. - block_hash: Optional block hash, when left to None the chain tip will be used. - - Returns - ------- - A two dimensional list of key-value pairs, both decoded into the given type, e.g. - [[k1, v1], [k2, v2], ...] - """ - await self.init_runtime(block_hash=block_hash) - - key_type = None - value_type = None - concat_hash_len = None - for metadata_module in self.metadata_decoder.metadata.modules: - if metadata_module.name == module: - if metadata_module.storage: - for storage_item in metadata_module.storage.items: - if storage_item.name == storage_function: - if 'MapType' in storage_item.type: - key_type = storage_item.type['MapType']['key'] - value_type = storage_item.type['MapType']['value'] - if storage_item.type['MapType']['hasher'] == "Blake2_128Concat": - concat_hash_len = 32 - elif storage_item.type['MapType']['hasher'] == "Twox64Concat": - concat_hash_len = 16 - else: - raise ValueError('Unsupported hash type') - else: - raise ValueError('Given storage is not a map') - - prefix = self.generate_storage_hash(module, storage_function) - prefix_len = len(prefix) - response = await self.factory.connection.async_rpc_request(method="state_getPairs", params=[prefix, block_hash]) - pairs = response.get('result') - - # convert keys to the portion that needs to be decoded. - pairs = map(lambda kp: ["0x" + kp[0][prefix_len + concat_hash_len:], kp[1]], pairs) - - # decode both of them - pairs = map( - lambda kp: [self.decode_scale(key_type, kp[0]), self.decode_scale(value_type, kp[1])], - list(pairs) - ) - - return list(pairs) - - async def get_runtime_state(self, module, storage_function, params=None, block_hash=None): - """ - Retrieves the storage entry for given module, function and optional parameters at given block hash - - Parameters - ---------- - module: The module name in the metadata, e.g. Balances or Account - storage_function: The storage function name, e.g. FreeBalance or AccountNonce - params: list of params, in the decoded format of the applicable ScaleTypes - block_hash: Optional block hash, when left to None the chain tip will be used - - Returns - ------- - - """ - - await self.init_runtime(block_hash=block_hash) - - # Search storage call in metadata - for metadata_module in self.metadata_decoder.metadata.modules: - if metadata_module.name == module: - if metadata_module.storage: - for storage_item in metadata_module.storage.items: - if storage_item.name == storage_function: - - key2_hasher = None - - if 'PlainType' in storage_item.type: - hasher = 'Twox64Concat' - return_scale_type = storage_item.type.get('PlainType') - if params: - raise ValueError('Storage call of type "PlainType" doesn\'t accept params') - - elif 'MapType' in storage_item.type: - - map_type = storage_item.type.get('MapType') - hasher = map_type.get('hasher') - return_scale_type = map_type.get('value') - - if not params or len(params) != 1: - raise ValueError('Storage call of type "MapType" requires 1 parameter') - - # Encode parameter - params[0] = self.convert_storage_parameter(map_type['key'], params[0]) - param_obj = ScaleDecoder.get_decoder_class(map_type['key']) - params[0] = param_obj.encode(params[0]) - - elif 'DoubleMapType' in storage_item.type: - - map_type = storage_item.type.get('DoubleMapType') - hasher = map_type.get('hasher') - key2_hasher = map_type.get('key2Hasher') - return_scale_type = map_type.get('value') - - if not params or len(params) != 2: - raise ValueError('Storage call of type "DoubleMapType" requires 2 parameters') - - # Encode parameter 1 - params[0] = self.convert_storage_parameter(map_type['key1'], params[0]) - param_obj = ScaleDecoder.get_decoder_class(map_type['key1']) - params[0] = param_obj.encode(params[0]) - - # Encode parameter 2 - params[1] = self.convert_storage_parameter(map_type['key2'], params[1]) - param_obj = ScaleDecoder.get_decoder_class(map_type['key2']) - params[1] = param_obj.encode(params[1]) - - else: - raise NotImplementedError("Storage type not implemented") - - storage_hash = self.generate_storage_hash( - storage_module=metadata_module.prefix, - storage_function=storage_function, - params=params, - hasher=hasher, - key2_hasher=key2_hasher, - metadata_version=self.metadata_decoder.version.index - ) - - response = await self.factory.connection.async_rpc_request("state_getStorageAt", [storage_hash, block_hash]) - - if 'result' in response: - - if return_scale_type and response.get('result'): - obj = ScaleDecoder.get_decoder_class( - return_scale_type, - ScaleBytes(response.get('result')), - metadata=self.metadata_decoder - ) - response['result'] = obj.decode() - - return response - - raise StorageFunctionNotFound('Storage function "{}.{}" not found'.format(module, storage_function)) - - def get_runtime_events(self, block_hash=None): - """ - Convenience method to get events for a certain block (storage call for module 'System' and function 'Events') - - Parameters - ---------- - block_hash - - Returns - ------- - Collection of events - """ - return self.get_runtime_state( - module="System", - storage_function="Events", - block_hash=block_hash - ) - - async def get_runtime_metadata(self, block_hash=None): - """ - Retrieves and decodes the metadata for given block or chaintip if block_hash is omitted. - - Parameters - ---------- - block_hash - - Returns - ------- - - """ - params = None - if block_hash: - params = [block_hash] - response = await self.factory.connection.async_rpc_request("state_getMetadata", params) - - if 'result' in response: - metadata_decoder = MetadataDecoder(ScaleBytes(response.get('result'))) - response['result'] = metadata_decoder.decode() - - return response - - async def compose_call(self, call_module, call_function, call_params=(), block_hash=None): - """ - Composes a call payload which can be used as an unsigned extrinsic or a proposal. - - Parameters - ---------- - call_module: Name of the runtime module e.g. Balances - call_function: Name of the call function e.g. transfer - call_params: This is a dict containing the params of the call. e.g. `{'dest': 'EaG2CRhJWPb7qmdcJvy3LiWdh26Jreu9Dx6R1rXxPmYXoDk', 'value': 1000000000000}` - block_hash - - Returns - ------- - - """ - await self.init_runtime(block_hash=block_hash) - - call = ScaleDecoder.get_decoder_class('Call', metadata=self.metadata_decoder) - - call.encode({ - 'call_module': call_module, - 'call_function': call_function, - 'call_args': call_params - }) - - return call - - async def get_account_nonce(self, account_address): - if account_address in self.nonce: - self.nonce[account_address] = self.nonce[account_address] + 1 - return self.nonce[account_address] - else: - response = await self.get_runtime_state('System', 'Account', [account_address]) - if response.get('result'): - self.nonce[account_address] = response['result'].get('nonce', 0) - return self.nonce[account_address] - return None - - async def generate_signature_payload(self, call, era=None, nonce=0, tip=0, include_call_length=False): - - # Retrieve genesis hash - genesis_hash = await self.get_block_hash(0) - - if not era: - era = '00' - - if era == '00': - block_hash = genesis_hash - else: - era_obj = ScaleDecoder.get_decoder_class('Era') - era_obj.encode(era) - block_hash = await self.get_block_hash(block_id=era_obj.birth(era.get('current'))) - - # Create signature payload - signature_payload = ScaleDecoder.get_decoder_class('ExtrinsicPayloadValue') - - if include_call_length: - - length_obj = RuntimeConfiguration().get_decoder_class('Bytes') - call_data = str(length_obj().encode(str(call.data))) - - else: - call_data = str(call.data) - - payload_dict = { - 'call': call_data, - 'era': era, - 'nonce': nonce, - 'tip': tip, - 'specVersion': self.runtime_version, - 'genesisHash': genesis_hash, - 'blockHash': block_hash - } - - if self.transaction_version is not None: - payload_dict['transactionVersion'] = self.transaction_version - - signature_payload.encode(payload_dict) - - if signature_payload.data.length > 256: - return ScaleBytes(data=blake2b(signature_payload.data.data, digest_size=32).digest()) - - return signature_payload.data - - async def create_signed_extrinsic(self, call, keypair: Keypair, era=None, nonce=None, tip=0, signature=None): - """ - Creates a extrinsic signed by given account details - - Parameters - ---------- - signature - era - keypair - call - nonce - tip - - Returns - ------- - ExtrinsicsDecoder The signed Extrinsic - """ - - # Check requirements - if not isinstance(call, GenericCall): - raise TypeError("'call' must be of type Call") - - # Retrieve nonce - if nonce is None: - nonce = await self.get_account_nonce(keypair.public_key) or 0 - - # Process era - if era is None: - era = '00' - else: - if isinstance(era, dict) and 'current' not in era and 'phase' not in era: - # Retrieve current block id - era['current'] = await self.get_block_number(await self.get_chain_finalised_head()) - - if signature is not None: - - signature = signature.replace('0x', '') - - # Check if signature is a MultiSignature and contains signature version - if len(signature) == 130: - signature_version = int(signature[0:2], 16) - signature = '0x{}'.format(signature[2:]) - else: - signature_version = keypair.crypto_type - - else: - # Create signature payload - signature_payload = await self.generate_signature_payload(call=call, era=era, nonce=nonce, tip=tip) - - # Set Signature version to crypto type of keypair - signature_version = keypair.crypto_type - - # Sign payload - signature = keypair.sign(signature_payload) - - # Create extrinsic - extrinsic = ScaleDecoder.get_decoder_class('Extrinsic', metadata=self.metadata_decoder) - - extrinsic.encode({ - 'account_id': keypair.public_key, - 'signature_version': signature_version, - 'signature': signature, - 'call_function': call.value['call_function'], - 'call_module': call.value['call_module'], - 'call_args': call.value['call_args'], - 'nonce': nonce, - 'era': era, - 'tip': tip - }) - - # Set extrinsic hash - extrinsic.extrinsic_hash = extrinsic.generate_hash() - - return extrinsic - - def create_unsigned_extrinsic(self, call): - # Create extrinsic - extrinsic = ScaleDecoder.get_decoder_class('Extrinsic', metadata=self.metadata_decoder) - - extrinsic.encode({ - 'call_function': call.value['call_function'], - 'call_module': call.value['call_module'], - 'call_args': call.value['call_args'] - }) - - return extrinsic - - async def submit_extrinsic(self, extrinsic, wait_for_inclusion=False, wait_for_finalization=False): - """ - - Parameters - ---------- - extrinsic: ExtrinsicsDecoder The extinsic to be send to the network - wait_for_inclusion: wait until extrinsic is included in a block (only works on websocket connections) - wait_for_finalization: wait until extrinsic is finalized (only works on websocket connections) - - Returns - ------- - The hash of the extrinsic submitted to the network - - """ - - # Check requirements - if extrinsic.__class__.__name__ != 'ExtrinsicsDecoder': - raise TypeError("'extrinsic' must be of type ExtrinsicsDecoder") - - def result_handler(result): - # Check if extrinsic is included and finalized - if 'params' in result and type(result['params']['result']) is dict: - if 'finalized' in result['params']['result'] and wait_for_finalization: - return { - 'block_hash': result['params']['result']['finalized'], - 'extrinsic_hash': '0x{}'.format(extrinsic.extrinsic_hash), - 'finalized': True - } - elif 'inBlock' in result['params']['result'] and wait_for_inclusion and not wait_for_finalization: - return { - 'block_hash': result['params']['result']['inBlock'], - 'extrinsic_hash': '0x{}'.format(extrinsic.extrinsic_hash), - 'finalized': False - } - - if wait_for_inclusion or wait_for_finalization: - response = await self.factory.connection.async_rpc_request( - "author_submitAndWatchExtrinsic", - [str(extrinsic.data)], - ) - return result_handler(response) - else: - response = await self.factory.connection.async_rpc_request("author_submitExtrinsic", [str(extrinsic.data)]) - - if 'result' not in response: - raise SubstrateRequestException(response.get('error')) - - response = { - 'extrinsic_hash': response['result'], - 'block_hash': None, - 'finalized': None - } - - return response - - async def get_payment_info(self, call, keypair): - """ - Retrieves fee estimation via RPC for given extrinsic - - Parameters - ---------- - call Call object to estimate fees for - keypair Keypair of the sender, does not have to include private key because no valid signature is required - - Returns - ------- - Dict with payment info - - E.g. {'class': 'normal', 'partialFee': 151000000, 'weight': 217238000} - - """ - - # Check requirements - if not isinstance(call, GenericCall): - raise TypeError("'call' must be of type Call") - - if not isinstance(keypair, Keypair): - raise TypeError("'keypair' must be of type Keypair") - - # No valid signature is required for fee estimation - signature = '0x' + '00' * 64 - - # Create extrinsic - extrinsic = self.create_signed_extrinsic( - call=call, - keypair=keypair, - signature=signature - ) - - payment_info = await self.factory.connection.async_rpc_request('payment_queryInfo', [str(extrinsic.data)]) - - # convert partialFee to int - if 'result' in payment_info: - payment_info['result']['partialFee'] = int(payment_info['result']['partialFee']) - return payment_info['result'] - else: - raise SubstrateRequestException(payment_info['error']['message']) - - def process_metadata_typestring(self, type_string): - """ - Process how given type_string is decoded with active runtime and type registry - - Parameters - ---------- - type_string: RUST variable type, e.g. Vec
- - Returns - ------- - - dict of properties for given type_string - - E.g. - - `{ - "type_string": "Vec
", - "decoder_class": "Vec", - "is_primitive_runtime": false, - "is_primitive_core": false, - "spec_version": 1030 - }` - - """ - decoder_class_obj = None - - type_info = { - "type_string": type_string, - "decoder_class": None, - "is_primitive_runtime": None, - "is_primitive_core": False, - "spec_version": self.runtime_version - } - - if self.runtime_version not in self.type_registry_cache: - self.type_registry_cache[self.runtime_version] = {} - - # Check if already added - if type_string.lower() in self.type_registry_cache[self.runtime_version]: - return self.type_registry_cache[self.runtime_version][type_string.lower()]['decoder_class'] - - # Try to get decoder class - decoder_class = RuntimeConfiguration().get_decoder_class(type_string) - - if not decoder_class: - - # Not in type registry, try get hard coded decoder classes - try: - decoder_class_obj = ScaleDecoder.get_decoder_class(type_string) - decoder_class = decoder_class_obj.__class__ - except NotImplementedError as e: - decoder_class = None - - # Process classes that contain subtypes (e.g. Option) - if decoder_class_obj and decoder_class_obj.sub_type: - type_info["is_primitive_runtime"] = False - - # Try to split on ',' (e.g. ActiveRecovery) - if not re.search('[<()>]', decoder_class_obj.sub_type): - for element in decoder_class_obj.sub_type.split(','): - if element not in ['T', 'I']: - self.process_metadata_typestring(element.strip()) - - # Process classes that contain type_mapping (e.g. Struct and Enum) - if decoder_class and hasattr(decoder_class, 'type_mapping') and decoder_class.type_mapping: - - if type_string[0] == '(': - type_info["is_primitive_runtime"] = False - - for key, data_type in decoder_class.type_mapping: - self.process_metadata_typestring(data_type) - - # Try to get superclass as actual decoding class if not root level 'ScaleType' - if decoder_class and len(decoder_class.__mro__) > 1 and decoder_class.__mro__[1].__name__ != 'ScaleType': - decoder_class = decoder_class.__mro__[1] - - if decoder_class: - type_info['decoder_class'] = decoder_class.__name__ - - if type_info["is_primitive_runtime"] is None: - type_info["is_primitive_runtime"] = True - - if type_info["is_primitive_runtime"] and type_string.lower() in ScaleDecoder.PRIMITIVES: - type_info["is_primitive_core"] = True - else: - type_info["is_primitive_runtime"] = None - type_info["is_primitive_core"] = None - - self.type_registry_cache[self.runtime_version][type_string.lower()] = type_info - - return decoder_class - - async def get_type_registry(self, block_hash=None): - """ - Generates an exhaustive list of which RUST types exist in the runtime specified at given block_hash (or - chaintip if block_hash is omitted) - - Parameters - ---------- - block_hash: Chaintip will be used if block_hash is omitted - - Returns - ------- - - """ - await self.init_runtime(block_hash=block_hash) - - if self.runtime_version not in self.type_registry_cache: - - for module in self.metadata_decoder.metadata.modules: - - # Storage backwards compt check - if module.storage and isinstance(module.storage, list): - storage_functions = module.storage - elif module.storage and isinstance(getattr(module.storage, 'value'), dict): - storage_functions = module.storage.items - else: - storage_functions = [] - - if len(module.calls or []) > 0: - for idx, call in enumerate(module.calls): - for arg in call.args: - self.process_metadata_typestring(arg.type) - - if len(module.events or []) > 0: - for event_index, event in enumerate(module.events): - - for arg_index, arg in enumerate(event.args): - self.process_metadata_typestring(arg) - - if len(storage_functions) > 0: - for idx, storage in enumerate(storage_functions): - - # Determine type - type_key1 = None - type_key2 = None - type_value = None - - if storage.type.get('PlainType'): - type_value = storage.type.get('PlainType') - - elif storage.type.get('MapType'): - type_key1 = storage.type['MapType'].get('key') - type_value = storage.type['MapType'].get('value') - - elif storage.type.get('DoubleMapType'): - type_key1 = storage.type['DoubleMapType'].get('key1') - type_key2 = storage.type['DoubleMapType'].get('key2') - type_value = storage.type['DoubleMapType'].get('value') - - self.process_metadata_typestring(type_value) - - if type_key1: - self.process_metadata_typestring(type_key1) - - if type_key2: - self.process_metadata_typestring(type_key2) - - if len(module.constants or []) > 0: - for idx, constant in enumerate(module.constants): - # Check if types already registered in database - self.process_metadata_typestring(constant.type) - - return self.type_registry_cache[self.runtime_version] - - def get_type_definition(self, type_string, block_hash=None): - """ - Retrieves decoding specifications of given type_string - - Parameters - ---------- - type_string: RUST variable type, e.g. Vec
- block_hash - - Returns - ------- - - """ - type_registry = self.get_type_registry(block_hash=block_hash) - return type_registry.get(type_string.lower()) - - async def get_metadata_modules(self, block_hash=None): - """ - Retrieves a list of modules in metadata for given block_hash (or chaintip if block_hash is omitted) - - Parameters - ---------- - block_hash - - Returns - ------- - - """ - await self.init_runtime(block_hash=block_hash) - - return [{ - 'metadata_index': idx, - 'module_id': module.get_identifier(), - 'name': module.name, - 'prefix': module.prefix, - 'spec_version': self.runtime_version, - 'count_call_functions': len(module.calls or []), - 'count_storage_functions': len(module.calls or []), - 'count_events': len(module.events or []), - 'count_constants': len(module.constants or []), - 'count_errors': len(module.errors or []), - } for idx, module in enumerate(self.metadata_decoder.metadata.modules)] - - async def get_metadata_call_functions(self, block_hash=None): - """ - Retrieves a list of all call functions in metadata active for given block_hash (or chaintip if block_hash is omitted) - - Parameters - ---------- - block_hash - - Returns - ------- - - """ - await self.init_runtime(block_hash=block_hash) - - call_list = [] - - for call_index, (module, call) in self.metadata_decoder.call_index.items(): - call_list.append( - self.serialize_module_call( - module, call, self.runtime_version, call_index - ) - ) - return call_list - - async def get_metadata_call_function(self, module_name, call_function_name, block_hash=None): - """ - Retrieves the details of a call function given module name, call function name and block_hash - (or chaintip if block_hash is omitted) - - Parameters - ---------- - module_name - call_function_name - block_hash - - Returns - ------- - - """ - await self.init_runtime(block_hash=block_hash) - - result = None - - for call_index, (module, call) in self.metadata_decoder.call_index.items(): - if module.name == module_name and \ - call.get_identifier() == call_function_name: - result = self.serialize_module_call( - module, call, self.runtime_version, call_index - ) - break - - return result - - async def get_metadata_events(self, block_hash=None): - """ - Retrieves a list of all events in metadata active for given block_hash (or chaintip if block_hash is omitted) - - Parameters - ---------- - block_hash - - Returns - ------- - - """ - - await self.init_runtime(block_hash=block_hash) - - event_list = [] - - for event_index, (module, event) in self.metadata_decoder.event_index.items(): - event_list.append( - self.serialize_module_event( - module, event, self.runtime_version, event_index - ) - ) - - return event_list - - async def get_metadata_event(self, module_name, event_name, block_hash=None): - """ - Retrieves the details of an event for given module name, call function name and block_hash - (or chaintip if block_hash is omitted) - - Parameters - ---------- - module_name - event_name - block_hash - - Returns - ------- - - """ - - await self.init_runtime(block_hash=block_hash) - - for event_index, (module, event) in self.metadata_decoder.event_index.items(): - if module.name == module_name and \ - event.name == event_name: - return self.serialize_module_event( - module, event, self.runtime_version, event_index - ) - - async def get_metadata_constants(self, block_hash=None): - """ - Retrieves a list of all constants in metadata active at given block_hash (or chaintip if block_hash is omitted) - - Parameters - ---------- - block_hash - - Returns - ------- - - """ - - await self.init_runtime(block_hash=block_hash) - - constant_list = [] - - for module_idx, module in enumerate(self.metadata_decoder.metadata.modules): - for constant in module.constants or []: - constant_list.append( - self.serialize_constant( - constant, module, self.runtime_version - ) - ) - - return constant_list - - async def get_metadata_constant(self, module_name, constant_name, block_hash=None): - """ - Retrieves the details of a constant for given module name, call function name and block_hash - (or chaintip if block_hash is omitted) - - Parameters - ---------- - module_name - constant_name - block_hash - - Returns - ------- - - """ - - await self.init_runtime(block_hash=block_hash) - - for module_idx, module in enumerate(self.metadata_decoder.metadata.modules): - - if module_name == module.name and module.constants: - - for constant in module.constants: - if constant_name == constant.name: - return self.serialize_constant( - constant, module, self.runtime_version - ) - - async def get_metadata_storage_functions(self, block_hash=None): - """ - Retrieves a list of all storage functions in metadata active at given block_hash (or chaintip if block_hash is - omitted) - - Parameters - ---------- - block_hash - - Returns - ------- - - """ - await self.init_runtime(block_hash=block_hash) - - storage_list = [] - - for module_idx, module in enumerate(self.metadata_decoder.metadata.modules): - if module.storage: - for storage in module.storage.items: - storage_list.append( - self.serialize_storage_item( - storage_item=storage, - module=module, - spec_version_id=self.runtime_version - ) - ) - - return storage_list - - async def get_metadata_storage_function(self, module_name, storage_name, block_hash=None): - """ - Retrieves the details of a storage function for given module name, call function name and block_hash - - Parameters - ---------- - module_name - storage_name - block_hash - - Returns - ------- - - """ - await self.init_runtime(block_hash=block_hash) - - for module_idx, module in enumerate(self.metadata_decoder.metadata.modules): - if module.name == module_name and module.storage: - for storage in module.storage.items: - if storage.name == storage_name: - return self.serialize_storage_item( - storage_item=storage, - module=module, - spec_version_id=self.runtime_version - ) - - async def get_metadata_errors(self, block_hash=None): - """ - Retrieves a list of all errors in metadata active at given block_hash (or chaintip if block_hash is omitted) - - Parameters - ---------- - block_hash - - Returns - ------- - - """ - await self.init_runtime(block_hash=block_hash) - - error_list = [] - - for module_idx, module in enumerate(self.metadata_decoder.metadata.modules): - if module.errors: - for error in module.errors: - error_list.append( - self.serialize_module_error( - module=module, error=error, spec_version=self.runtime_version - ) - ) - - return error_list - - async def get_metadata_error(self, module_name, error_name, block_hash=None): - """ - Retrieves the details of an error for given module name, call function name and block_hash - - Parameters - ---------- - module_name - error_name - block_hash - - Returns - ------- - - """ - await self.init_runtime(block_hash=block_hash) - - for module_idx, module in enumerate(self.metadata_decoder.metadata.modules): - if module.name == module_name and module.errors: - for error in module.errors: - if error_name == error.name: - return self.serialize_module_error( - module=module, error=error, spec_version=self.runtime_version - ) - - async def get_runtime_block(self, block_hash=None, block_id=None): - """ - Retrieves a block with method `chain_getBlock` and in addition decodes extrinsics and log items - - Parameters - ---------- - block_hash - block_id - - Returns - ------- - - """ - await self.init_runtime(block_hash=block_hash, block_id=block_id) - - response = await self.factory.connection.async_rpc_request("chain_getBlock", [self.block_hash]).get('result') - - response['block']['header']['number'] = int(response['block']['header']['number'], 16) - - for idx, extrinsic_data in enumerate(response['block']['extrinsics']): - extrinsic_decoder = ExtrinsicsDecoder( - data=ScaleBytes(extrinsic_data), - metadata=self.metadata_decoder - ) - extrinsic_decoder.decode() - response['block']['extrinsics'][idx] = extrinsic_decoder.value - - for idx, log_data in enumerate(response['block']['header']["digest"]["logs"]): - log_digest = LogDigest(ScaleBytes(log_data)) - log_digest.decode() - response['block']['header']["digest"]["logs"][idx] = log_digest.value - - return response - - def decode_scale(self, type_string, scale_bytes, block_hash=None): - """ - Helper function to decode arbitrary SCALE-bytes (e.g. 0x02000000) according to given RUST type_string - (e.g. BlockNumber). The relevant versioning information of the type (if defined) will be applied if block_hash - is set - - Parameters - ---------- - type_string - scale_bytes - block_hash - - Returns - ------- - - """ - #await self.init_runtime(block_hash=block_hash) - - obj = ScaleDecoder.get_decoder_class(type_string, ScaleBytes(scale_bytes), metadata=self.metadata_decoder) - return obj.decode() - - async def encode_scale(self, type_string, value, block_hash=None): - """ - Helper function to encode arbitrary data into SCALE-bytes for given RUST type_string - - Parameters - ---------- - type_string - value - block_hash - - Returns - ------- - - """ - await self.init_runtime(block_hash=block_hash) - - obj = ScaleDecoder.get_decoder_class(type_string) - return str(obj.encode(value)) - - # Serializing helper function - - def serialize_storage_item(self, storage_item, module, spec_version_id): - """ - Helper function to serialize a storage item - - Parameters - ---------- - storage_item - module - spec_version_id - - Returns - ------- - - """ - storage_dict = { - "storage_name": storage_item.name, - "storage_modifier": storage_item.modifier, - "storage_fallback_scale": storage_item.fallback, - "storage_fallback": None, - "documentation": '\n'.join(storage_item.docs), - "module_id": module.get_identifier(), - "module_prefix": module.prefix, - "module_name": module.name, - "spec_version": spec_version_id, - "type_key1": None, - "type_key2": None, - "type_hasher_key1": None, - "type_hasher_key2": None, - "type_value": None, - "type_is_linked": None - } - - type_class, type_info = next(iter(storage_item.type.items())) - - storage_dict["type_class"] = type_class - - if type_class == 'PlainType': - storage_dict["type_value"] = type_info - - elif type_class == 'MapType': - storage_dict["type_value"] = type_info["value"] - storage_dict["type_key1"] = type_info["key"] - storage_dict["type_hasher_key1"] = type_info["hasher"] - storage_dict["type_is_linked"] = type_info["isLinked"] - - elif type_class == 'DoubleMapType': - - storage_dict["type_value"] = type_info["value"] - storage_dict["type_key1"] = type_info["key1"] - storage_dict["type_key2"] = type_info["key2"] - storage_dict["type_hasher_key1"] = type_info["hasher"] - storage_dict["type_hasher_key2"] = type_info["key2Hasher"] - - if storage_item.fallback != '0x00': - # Decode fallback - try: - fallback_obj = ScaleDecoder.get_decoder_class(storage_dict["type_value"], - ScaleBytes(storage_item.fallback)) - storage_dict["storage_fallback"] = fallback_obj.decode() - except Exception: - storage_dict["storage_fallback"] = '[decoding error]' - - return storage_dict - - def serialize_constant(self, constant, module, spec_version_id): - """ - Helper function to serialize a constant - - Parameters - ---------- - constant - module - spec_version_id - - Returns - ------- - - """ - try: - value_obj = ScaleDecoder.get_decoder_class(constant.type, - ScaleBytes(constant.constant_value)) - constant_decoded_value = value_obj.decode() - except Exception: - constant_decoded_value = '[decoding error]' - - return { - "constant_name": constant.name, - "constant_type": constant.type, - "constant_value": constant_decoded_value, - "constant_value_scale": constant.constant_value, - "documentation": '\n'.join(constant.docs), - "module_id": module.get_identifier(), - "module_prefix": module.prefix, - "module_name": module.name, - "spec_version": spec_version_id - } - - def serialize_module_call(self, module, call, spec_version, call_index): - """ - Helper function to serialize a call function - - Parameters - ---------- - module - call - spec_version - call_index - - Returns - ------- - - """ - return { - "call_id": call.get_identifier(), - "call_name": call.name, - "call_args": [call_arg.value for call_arg in call.args], - "lookup": '0x{}'.format(call_index), - "documentation": '\n'.join(call.docs), - "module_id": module.get_identifier(), - "module_prefix": module.prefix, - "module_name": module.name, - "spec_version": spec_version - } - - def serialize_module_event(self, module, event, spec_version, event_index): - """ - Helper function to serialize an event - - Parameters - ---------- - module - event - spec_version - event_index - - Returns - ------- - - """ - return { - "event_id": event.name, - "event_name": event.name, - "event_args": [ - { - "event_arg_index": idx, - "type": arg - } for idx, arg in enumerate(event.args) - ], - "lookup": '0x{}'.format(event_index), - "documentation": '\n'.join(event.docs), - "module_id": module.get_identifier(), - "module_prefix": module.prefix, - "module_name": module.name, - "spec_version": spec_version - } - - def serialize_module_error(self, module, error, spec_version): - """ - Helper function to serialize an error - - Parameters - ---------- - module - error - spec_version - - Returns - ------- - - """ - return { - "error_name": error.name, - "documentation": '\n'.join(error.docs), - "module_id": module.get_identifier(), - "module_prefix": module.prefix, - "module_name": module.name, - "spec_version": spec_version - } - - def update_type_registry_presets(self): - try: - update_type_registries() - return True - except Exception: - return False - - def debug_message(self, message): - logger.debug(message) - - - - - - -class SubstrateRPCInterface: - # Todo - None - diff --git a/bittensor/subtensor/interface/__pycache__/__init__.cpython-38.pyc b/bittensor/subtensor/interface/__pycache__/__init__.cpython-38.pyc deleted file mode 100644 index e19f9bcc8f..0000000000 Binary files a/bittensor/subtensor/interface/__pycache__/__init__.cpython-38.pyc and /dev/null differ diff --git a/bittensor/subtensor/interface/__pycache__/constants.cpython-38.pyc b/bittensor/subtensor/interface/__pycache__/constants.cpython-38.pyc deleted file mode 100644 index f5bd04a057..0000000000 Binary files a/bittensor/subtensor/interface/__pycache__/constants.cpython-38.pyc and /dev/null differ diff --git a/bittensor/subtensor/interface/__pycache__/exceptions.cpython-38.pyc b/bittensor/subtensor/interface/__pycache__/exceptions.cpython-38.pyc deleted file mode 100644 index 60e991efe5..0000000000 Binary files a/bittensor/subtensor/interface/__pycache__/exceptions.cpython-38.pyc and /dev/null differ diff --git a/bittensor/subtensor/interface/__pycache__/key.cpython-38.pyc b/bittensor/subtensor/interface/__pycache__/key.cpython-38.pyc deleted file mode 100644 index ac2fb1c061..0000000000 Binary files a/bittensor/subtensor/interface/__pycache__/key.cpython-38.pyc and /dev/null differ diff --git a/bittensor/subtensor/interface/__pycache__/subkey.cpython-38.pyc b/bittensor/subtensor/interface/__pycache__/subkey.cpython-38.pyc deleted file mode 100644 index d4865e0d22..0000000000 Binary files a/bittensor/subtensor/interface/__pycache__/subkey.cpython-38.pyc and /dev/null differ diff --git a/bittensor/subtensor/interface/constants.py b/bittensor/subtensor/interface/constants.py deleted file mode 100644 index 4258cf4f59..0000000000 --- a/bittensor/subtensor/interface/constants.py +++ /dev/null @@ -1,20 +0,0 @@ -# Python Substrate Interface Library -# -# Copyright 2018-2020 Stichting Polkascan (Polkascan Foundation). -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -STORAGE_HASH_SYSTEM_EVENTS = "0xcc956bdb7605e3547539f321ac2bc95c" -STORAGE_HASH_SYSTEM_EVENTS_V9 = "0x26aa394eea5630e07c48ae0c9558cef780d41e5e16056765bc8461851072c9d7" - -DEV_PHRASE = 'bottom drive obey lake curtain smoke basket hold race lonely fit walk' diff --git a/bittensor/subtensor/interface/exceptions.py b/bittensor/subtensor/interface/exceptions.py deleted file mode 100644 index 68f483ff27..0000000000 --- a/bittensor/subtensor/interface/exceptions.py +++ /dev/null @@ -1,27 +0,0 @@ -# Python Substrate Interface Library -# -# Copyright 2018-2020 Stichting Polkascan (Polkascan Foundation). -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -class SubstrateRequestException(Exception): - pass - - -class StorageFunctionNotFound(ValueError): - pass - - -class ConfigurationError(Exception): - pass diff --git a/bittensor/subtensor/interface/key.py b/bittensor/subtensor/interface/key.py deleted file mode 100644 index ab463d2006..0000000000 --- a/bittensor/subtensor/interface/key.py +++ /dev/null @@ -1,61 +0,0 @@ -# Python Substrate Interface Library -# -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import re -from hashlib import blake2b - -from scalecodec import ScaleDecoder - -RE_JUNCTION = r'(\/\/?)([^/]+)' -JUNCTION_ID_LEN = 32 - - -class DeriveJunction: - def __init__(self, chain_code, is_hard=False): - self.chain_code = chain_code - self.is_hard = is_hard - - @classmethod - def from_derive_path(cls, path: str, is_hard=False): - - path_scale = ScaleDecoder.get_decoder_class('Bytes') - path_scale.encode(path) - - if len(path) > JUNCTION_ID_LEN: - chain_code = blake2b(path_scale.data.data, digest_size=32).digest() - else: - chain_code = bytes(path_scale.data.data.ljust(32, b'\x00')) - - return cls(chain_code=chain_code, is_hard=is_hard) - - -def extract_derive_path(derive_path: str): - - path_check = '' - junctions = [] - paths = re.findall(RE_JUNCTION, derive_path) - - if paths: - path_check = ''.join(''.join(path) for path in paths) - - for path_separator, path_value in paths: - junctions.append(DeriveJunction.from_derive_path( - path=path_value, is_hard=path_separator == '//') - ) - - if path_check != derive_path: - raise ValueError('Reconstructed path "{}" does not match input'.format(path_check)) - - return junctions - diff --git a/bittensor/subtensor/interface/subkey.py b/bittensor/subtensor/interface/subkey.py deleted file mode 100644 index a55e7a10ca..0000000000 --- a/bittensor/subtensor/interface/subkey.py +++ /dev/null @@ -1,152 +0,0 @@ -# Python Subkey Wrapper -# -# Copyright 2018-2020 Stichting Polkascan (Polkascan Foundation). -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import json -import shlex -import subprocess -from abc import ABC, abstractmethod - -import docker -from docker.errors import ContainerError - - -class CommandFailException(Exception): - pass - - -class InvalidConfigurationError(Exception): - pass - - -class SubkeyImplementation(ABC): - - @abstractmethod - def execute_command(self, command, stdin=None, json_output=True, **kwargs): - pass - - def generate_key(self, network): - return self.execute_command(['generate', '--network={}'.format(network)]) - - def inspect(self, network, suri): - return self.execute_command(['inspect', '--network={}'.format(network), suri]) - - def vanity(self, network, pattern): - return self.execute_command(['vanity', '--pattern={}'.format(pattern), '--network={}'.format(network)]) - - def sign(self, data, suri, is_hex=True): - - return self.execute_command( - command=['sign', '--hex', suri], - stdin=data, - json_output=False - ) - - -class DockerSubkeyImplementation(SubkeyImplementation): - - def __init__(self, docker_image=None): - - self.docker_image = docker_image or 'parity/subkey:latest' - - def execute_command(self, command, stdin=None, json_output=True, **kwargs): - - if json_output: - command = command + ['--output-type=json'] - - full_command = ' '.join([shlex.quote(el) for el in command]) - - if stdin: - full_command = '-c "echo -n \\"{}\\" | subkey {}"'.format(stdin, full_command) - else: - full_command = '-c "subkey {}"'.format(full_command) - - client = docker.from_env() - try: - output = client.containers.run(self.docker_image, full_command, entrypoint='/bin/sh') - - output = output[0:-1].decode() - - if json_output: - output = json.loads(output[output.index('{'):]) - - return output - - except ContainerError as e: - raise CommandFailException('Docker Error: ', e) - - except json.JSONDecodeError as e: - raise CommandFailException('Invalid format: ', e) - - -class LocalSubkeyImplementation(SubkeyImplementation): - - def __init__(self, subkey_path=None): - self.subkey_path = subkey_path - - def execute_command(self, command, stdin=None, json_output=True, **kwargs): - - result = subprocess.run([self.subkey_path] + command + ['--output-type', 'json'], input=stdin, encoding='ascii', - stdout=subprocess.PIPE, stderr=subprocess.PIPE) - - if result.returncode > 0: - raise CommandFailException(result.stderr) - - # Strip the newline in the end of the result - output = result.stdout[0:-1] - - if json_output: - output = json.loads(output) - - return output - - -class HttpSubkeyImplementation(SubkeyImplementation): - - def execute_command(self, command, stdin=None, json_output=True, **kwargs): - pass - - -class Subkey: - - def __init__(self, use_docker=True, docker_image=None, subkey_path=None, subkey_host=None): - - if subkey_path: - self.implementation = LocalSubkeyImplementation(subkey_path=subkey_path) - elif subkey_host: - self.implementation = HttpSubkeyImplementation() - elif use_docker: - self.implementation = DockerSubkeyImplementation(docker_image=docker_image) - else: - raise InvalidConfigurationError( - 'No valid subkey configuration, either set subkey_path, subkey_host or use_docker' - ) - - def execute_command(self, command): - self.implementation.execute_command(command) - - def generate_key(self, network): - return self.implementation.generate_key(network=network) - - def vanity(self, network, pattern): - return self.implementation.vanity(network=network, pattern=pattern) - - def inspect(self, network, suri): - return self.implementation.inspect(network=network, suri=suri) - - def sign(self, data, suri, is_hex=True): - if is_hex: - data = data.replace('0x', '') - return self.implementation.sign(data=data, suri=suri, is_hex=is_hex) diff --git a/bittensor/subtensor/interface/utils/__init__.py b/bittensor/subtensor/interface/utils/__init__.py deleted file mode 100644 index d46465a919..0000000000 --- a/bittensor/subtensor/interface/utils/__init__.py +++ /dev/null @@ -1,17 +0,0 @@ -# Python Substrate Interface Library -# -# Copyright 2018-2020 Stichting Polkascan (Polkascan Foundation). -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - diff --git a/bittensor/subtensor/interface/utils/__pycache__/__init__.cpython-38.pyc b/bittensor/subtensor/interface/utils/__pycache__/__init__.cpython-38.pyc deleted file mode 100644 index d0919375fe..0000000000 Binary files a/bittensor/subtensor/interface/utils/__pycache__/__init__.cpython-38.pyc and /dev/null differ diff --git a/bittensor/subtensor/interface/utils/__pycache__/hasher.cpython-38.pyc b/bittensor/subtensor/interface/utils/__pycache__/hasher.cpython-38.pyc deleted file mode 100644 index 4f8bf45069..0000000000 Binary files a/bittensor/subtensor/interface/utils/__pycache__/hasher.cpython-38.pyc and /dev/null differ diff --git a/bittensor/subtensor/interface/utils/__pycache__/ss58.cpython-38.pyc b/bittensor/subtensor/interface/utils/__pycache__/ss58.cpython-38.pyc deleted file mode 100644 index 2af9951745..0000000000 Binary files a/bittensor/subtensor/interface/utils/__pycache__/ss58.cpython-38.pyc and /dev/null differ diff --git a/bittensor/subtensor/interface/utils/hasher.py b/bittensor/subtensor/interface/utils/hasher.py deleted file mode 100644 index f3b52a5e87..0000000000 --- a/bittensor/subtensor/interface/utils/hasher.py +++ /dev/null @@ -1,118 +0,0 @@ -# Python Substrate Interface Library -# -# Copyright 2018-2020 Stichting Polkascan (Polkascan Foundation). -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" Helper functions used to calculate keys for Substrate storage items -""" - -from hashlib import blake2b -import xxhash - - -def blake2_256(data): - """ - Helper function to calculate a 32 bytes Blake2b hash for provided data, used as key for Substrate storage items - - Parameters - ---------- - data - - Returns - ------- - - """ - return blake2b(data, digest_size=32).digest().hex() - - -def blake2_128(data): - """ - Helper function to calculate a 16 bytes Blake2b hash for provided data, used as key for Substrate storage items - - Parameters - ---------- - data - - Returns - ------- - - """ - return blake2b(data, digest_size=16).digest().hex() - - -def blake2_128_concat(data): - """ - Helper function to calculate a 16 bytes Blake2b hash for provided data, concatenated with data, used as key - for Substrate storage items - - Parameters - ---------- - data - - Returns - ------- - - """ - return "{}{}".format(blake2b(data, digest_size=16).digest().hex(), data.hex()) - - -def xxh128(data): - """ - Helper function to calculate a 2 concatenated xxh64 hash for provided data, used as key for several Substrate - - Parameters - ---------- - data - - Returns - ------- - - """ - storage_key1 = bytearray(xxhash.xxh64(data, seed=0).digest()) - storage_key1.reverse() - - storage_key2 = bytearray(xxhash.xxh64(data, seed=1).digest()) - storage_key2.reverse() - - return "{}{}".format(storage_key1.hex(), storage_key2.hex()) - - -def two_x64_concat(data): - """ - Helper function to calculate a xxh64 hash with concatenated data for provided data, - used as key for several Substrate - - Parameters - ---------- - data - - Returns - ------- - - """ - storage_key = bytearray(xxhash.xxh64(data, seed=0).digest()) - storage_key.reverse() - - return "{}{}".format(storage_key.hex(), data.hex()) - - -def xxh64(data): - storage_key = bytearray(xxhash.xxh64(data, seed=0).digest()) - storage_key.reverse() - - return "{}".format(storage_key.hex()) - - -def identity(data): - return data.hex() diff --git a/bittensor/subtensor/interface/utils/ss58.py b/bittensor/subtensor/interface/utils/ss58.py deleted file mode 100644 index 2ea50520c2..0000000000 --- a/bittensor/subtensor/interface/utils/ss58.py +++ /dev/null @@ -1,165 +0,0 @@ -# Python Substrate Interface Library -# -# Copyright 2018-2020 Stichting Polkascan (Polkascan Foundation). -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ss58.py - -""" SS58 is a simple address format designed for Substrate based chains. - Encoding/decoding according to specification on https://wiki.parity.io/External-Address-Format-(SS58) - -""" -import base58 -from hashlib import blake2b - -from scalecodec import ScaleBytes -from scalecodec.types import U8, U16, U32, U64 - - -def ss58_decode(address, valid_address_type=None): - """ - Decodes given SS58 encoded address to an account ID - Parameters - ---------- - address: e.g. EaG2CRhJWPb7qmdcJvy3LiWdh26Jreu9Dx6R1rXxPmYXoDk - valid_address_type - - Returns - ------- - Decoded string AccountId - """ - checksum_prefix = b'SS58PRE' - - ss58_format = base58.b58decode(address) - - if valid_address_type and ss58_format[0] != valid_address_type: - raise ValueError("Invalid Address type") - - # Determine checksum length according to length of address string - if len(ss58_format) in [3, 4, 6, 10]: - checksum_length = 1 - elif len(ss58_format) in [5, 7, 11, 35]: - checksum_length = 2 - elif len(ss58_format) in [8, 12]: - checksum_length = 3 - elif len(ss58_format) in [9, 13]: - checksum_length = 4 - elif len(ss58_format) in [14]: - checksum_length = 5 - elif len(ss58_format) in [15]: - checksum_length = 6 - elif len(ss58_format) in [16]: - checksum_length = 7 - elif len(ss58_format) in [17]: - checksum_length = 8 - else: - raise ValueError("Invalid address length") - - checksum = blake2b(checksum_prefix + ss58_format[0:-checksum_length]).digest() - - if checksum[0:checksum_length] != ss58_format[-checksum_length:]: - raise ValueError("Invalid checksum") - - return ss58_format[1:len(ss58_format)-checksum_length].hex() - - -def ss58_encode(address, address_type=42): - """ - Encodes an account ID to an Substrate address according to provided address_type - - Parameters - ---------- - address - address_type - - Returns - ------- - - """ - checksum_prefix = b'SS58PRE' - - if type(address) is bytes or type(address) is bytearray: - address_bytes = address - else: - address_bytes = bytes.fromhex(address.replace('0x', '')) - - if len(address_bytes) == 32: - # Checksum size is 2 bytes for public key - checksum_length = 2 - elif len(address_bytes) in [1, 2, 4, 8]: - # Checksum size is 1 byte for account index - checksum_length = 1 - else: - raise ValueError("Invalid length for address") - - address_format = bytes([address_type]) + address_bytes - checksum = blake2b(checksum_prefix + address_format).digest() - - return base58.b58encode(address_format + checksum[:checksum_length]).decode() - - -def ss58_encode_account_index(account_index, address_type=42): - """ - Encodes an AccountIndex to an Substrate address according to provided address_type - - Parameters - ---------- - account_index - address_type - - Returns - ------- - - """ - - if 0 <= account_index <= 2**8 - 1: - account_idx_encoder = U8() - elif 2**8 <= account_index <= 2**16 - 1: - account_idx_encoder = U16() - elif 2**16 <= account_index <= 2**32 - 1: - account_idx_encoder = U32() - elif 2**32 <= account_index <= 2**64 - 1: - account_idx_encoder = U64() - else: - raise ValueError("Value too large for an account index") - - return ss58_encode(account_idx_encoder.encode(account_index).data, address_type) - - -def ss58_decode_account_index(address, valid_address_type=42): - """ - Decodes given SS58 encoded address to an AccountIndex - - Parameters - ---------- - address - valid_address_type - - Returns - ------- - Decoded int AccountIndex - """ - account_index_bytes = ss58_decode(address, valid_address_type) - - if len(account_index_bytes) == 2: - return U8(ScaleBytes('0x{}'.format(account_index_bytes))).decode() - if len(account_index_bytes) == 4: - return U16(ScaleBytes('0x{}'.format(account_index_bytes))).decode() - if len(account_index_bytes) == 8: - return U32(ScaleBytes('0x{}'.format(account_index_bytes))).decode() - if len(account_index_bytes) == 16: - return U64(ScaleBytes('0x{}'.format(account_index_bytes))).decode() - else: - raise ValueError("Invalid account index length") - diff --git a/bittensor/synapse.py b/bittensor/synapse.py deleted file mode 100644 index 9f81589bff..0000000000 --- a/bittensor/synapse.py +++ /dev/null @@ -1,217 +0,0 @@ -import argparse -from loguru import logger -from munch import Munch -import random -import torch -import torch.nn as nn -import torch.optim as optim -from typing import List, Tuple, Dict, Optional, TYPE_CHECKING - -import bittensor -from bittensor import bittensor_pb2 -from bittensor.exceptions.handlers import rollbar - -class SynapseOutput(object): - """ Synapse output container. - loss (:obj:`List[str]` of shape :obj:`(batch_size)`, `required`): - Total loss acumulation to be used by loss.backward() - - local_hidden (:obj:`torch.FloatTensor` of shape :obj:`(batch_size, bittensor.__network_dim__)`, `required`): - Hidden layer encoding produced using the local_context. - - local_target (:obj:`torch.FloatTensor` of shape :obj:`(batch_size, target_dim)`, `optional`): - Target predictions using local_context. - - local_target_loss (:obj:`torch.FloatTensor` of shape :obj:`(1)`, `optional`): - Target loss using the local_context. - - remote_hidden (:obj:`torch.FloatTensor` of shape :obj:`(batch_size, bittensor.__network_dim__)`, `optional`): - Hidden layer encoding produced using the remote_context. - - remote_target (:obj:`torch.FloatTensor` of shape :obj:`(batch_size, target_dim)`, `optional`): - FFNN Target predictions using the remote_context. - - remote_target_loss (:obj:`torch.FloatTensor` of shape :obj:`(1)`, `optional`): - FFNN Classification loss using the remote_context. - - distillation_loss (:obj:`torch.FloatTensor` of shape :obj:`(1)`, `optional`): - Distillation loss between local_context and remote_context. - - weights (:obj:`torch.FloatTensor` of shape :obj:`(batch_size, metagraph.state.n)`, `optional`): - weights for each active neuron. - - requests_sizes (:obj:`torch.LongTensor` of shape :obj:`(metagraph.state.n)`, `optional`): - number of requests sent to each uid in this batch. - - return_codes (:obj:`List[torch.LongTensor]` of shape :obj:`[num_neurons]`, `required`): - dendrite return codes. 0 for success. - - metadata (:obj:`dict {'accuracy', torch.FloatTensor} ` of shape :obj:`(1)`, `optional`): - additional metadata output, specifically accuracy. - - """ - def __init__( - self, - loss: torch.Tensor = None, - local_hidden: torch.Tensor = None, - local_target: torch.Tensor = None, - local_target_loss: torch.Tensor = None, - remote_hidden: torch.Tensor = None, - remote_target: torch.Tensor = None, - remote_target_loss: torch.Tensor = None, - distillation_loss: torch.Tensor = None, - weights: torch.Tensor = None, - metadata: dict = None - ): - self.loss = loss - self.local_hidden = local_hidden - self.local_target = local_target - self.local_target_loss = local_target_loss - self.remote_hidden = remote_hidden - self.remote_target = remote_target - self.remote_target_loss = remote_target_loss - self.distillation_loss = distillation_loss - self.weights = weights - if metadata == None: - self.metadata = {} - else: - self.metadata = metadata - -class Synapse(nn.Module): - """ Bittensor synapse class. - - """ - - def __init__( self, - config: Munch, - session): - r""" Init synapse module. - - Args: - config (:obj:`SynapseConfig`, `required`): - Base synapse config configuration class. - - session (:obj:`bittensor.Session`, `optional`): - bittensor training session. - """ - super().__init__() - - self.config = config - self.session = session - self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu") - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - pass - - @staticmethod - def check_config(config: Munch): - pass - - def deepcopy(self): - """ Returns a copy of this synapse by passing the model params to load_state_dict. - - Returns: - synapse_copy (:obj:`self.__class__`, `required`): - Deep copy synapse object. - """ - SynapseClass = self.__class__ - synapse_copy = SynapseClass(self.config, self.session) - synapse_copy.load_state_dict(self.state_dict()) - return synapse_copy - - def call_forward(self, inputs: torch.Tensor, modality: bittensor_pb2.Modality, no_grad=True) -> torch.Tensor: - """ - Apply forward pass to the bittensor.synapse given inputs and modality. - """ - if no_grad: - with torch.no_grad(): - if modality == bittensor_pb2.Modality.TEXT: - outputs = self.forward_text(inputs) - elif modality == bittensor_pb2.Modality.IMAGE: - outputs = self.forward_image(inputs) - elif modality == bittensor_pb2.Modality.TENSOR: - outputs = self.forward_tensor(inputs) - else: - raise NotImplementedError - else: - if modality == bittensor_pb2.Modality.TEXT: - outputs = self.forward_text(inputs) - elif modality == bittensor_pb2.Modality.IMAGE: - outputs = self.forward_image(inputs) - elif modality == bittensor_pb2.Modality.TENSOR: - outputs = self.forward_tensor(inputs) - else: - raise NotImplementedError - - return outputs - - def grad(self, inputs_x: torch.Tensor, grads_dy: torch.Tensor, modality: bittensor_pb2.Modality) -> torch.Tensor: - """ - Returns gradients for the inputs given inputs and output grads. - """ - with torch.enable_grad(): - outputs_y = self.call_forward(inputs = inputs_x.to(self.device), modality = modality, no_grad=False) - grads_dx = torch.autograd.grad( - outputs = outputs_y.to(self.device), - inputs = inputs_x.to(self.device), - grad_tensors = grads_dy.to(self.device), - only_inputs = True, - create_graph = False, - retain_graph = False - ) - return grads_dx - - def backward(self, inputs_x: torch.Tensor, grads_dy: torch.Tensor, modality: bittensor_pb2.Modality): - """ - Apply a backward pass to the nn.module given grads and inputs. - """ - with torch.enable_grad(): - outputs_y = self.call_forward(inputs = inputs_x.to(self.device), modality = modality, no_grad=False) - torch.autograd.backward( - tensors = [outputs_y.to(self.device)], - grad_tensors = [grads_dy.to(self.device)], - create_graph = False, - retain_graph=False - ) - - def forward_text(self, inputs: torch.Tensor) -> SynapseOutput: - """ Local forward inputs through the bittensor.Synapse. To be implemented by sub-classes. - - Args: - inputs (:obj:`torch.LongTensor` of shape :obj:`(batch_size, sequence_len)`, `required`): - Batch_size length list of text sentences. - - Returns: - local_output (:obj:`torch.FloatTensor` of shape :obj:`(batch_size, sequence_len, bittensor.__network_dim__)`, `required`): - Output encoding of inputs produced by the synapse. - """ - raise NotImplementedError - - def forward_image(self, inputs: torch.Tensor) -> SynapseOutput: - r""" Forward pass inputs through the bittensor.synapse. To be implemented by sub-classes. - - Args: - inputs (:obj:`torch.FloatTensor` of shape :obj:`(batch_size, sequence_dim, channels, rows, cols)`, `required`): - batch_size list of image tensors. (batch index, sequence_len, channel, row, col) produced for images - by calling PIL.toTensor(). - - Returns: - local_output (:obj:`torch.FloatTensor` of shape :obj:`(batch_size, sequence_dim, bittensor.network_size)`, `required`): - Output encoding of inputs produced by the synapse. - """ - raise NotImplementedError - - def forward_tensor(self, inputs: torch.Tensor) -> SynapseOutput: - """ Forward tensor inputs through the bittensor.synapse. To be implemented by sub-classes. - - Args: - inputs (:obj:`torch.LongTensor` of shape :obj:`(batch_size, sequence_len, bittensor.__network_dim__)`, `required`): - Batch_size length sequences of tensors. - - Returns: - local_output (:obj:`torch.FloatTensor` of shape :obj:`(batch_size, sequence_len, bittensor.__network_dim__)`, `required`): - Output encoding of inputs produced by the synapse. - """ - raise NotImplementedError - diff --git a/bittensor/synapses/__pycache__/__init__.cpython-38.pyc b/bittensor/synapses/__pycache__/__init__.cpython-38.pyc deleted file mode 100644 index fbeec0f589..0000000000 Binary files a/bittensor/synapses/__pycache__/__init__.cpython-38.pyc and /dev/null differ diff --git a/bittensor/synapses/__pycache__/bert.cpython-38.pyc b/bittensor/synapses/__pycache__/bert.cpython-38.pyc deleted file mode 100644 index 501c29c4f2..0000000000 Binary files a/bittensor/synapses/__pycache__/bert.cpython-38.pyc and /dev/null differ diff --git a/bittensor/synapses/__pycache__/dpn.cpython-38.pyc b/bittensor/synapses/__pycache__/dpn.cpython-38.pyc deleted file mode 100644 index f6d33384ae..0000000000 Binary files a/bittensor/synapses/__pycache__/dpn.cpython-38.pyc and /dev/null differ diff --git a/bittensor/synapses/__pycache__/ffnn.cpython-38.pyc b/bittensor/synapses/__pycache__/ffnn.cpython-38.pyc deleted file mode 100644 index 43666caf87..0000000000 Binary files a/bittensor/synapses/__pycache__/ffnn.cpython-38.pyc and /dev/null differ diff --git a/bittensor/synapses/__pycache__/gpt2.cpython-38.pyc b/bittensor/synapses/__pycache__/gpt2.cpython-38.pyc deleted file mode 100644 index 9ae26a96c8..0000000000 Binary files a/bittensor/synapses/__pycache__/gpt2.cpython-38.pyc and /dev/null differ diff --git a/bittensor/synapses/bert.py b/bittensor/synapses/bert.py deleted file mode 100644 index fb669f5669..0000000000 --- a/bittensor/synapses/bert.py +++ /dev/null @@ -1,534 +0,0 @@ -import bittensor -from bittensor.dendrites.pkm import PKMDendrite -from bittensor.synapse import Synapse -from bittensor.synapse import SynapseOutput -from bittensor.session import Session - -import argparse -import random -import torch -import torch.nn.functional as F -import transformers -from transformers import BertModel, BertConfig -from munch import Munch - -class BertSynapseBase (Synapse): - def __init__( self, - config: Munch, - session: Session): - r""" Init a new base-bert synapse. - - Args: - config (:obj:`munch.Munch`, `required`): - BertNSP configuration class. - - Session (:obj:`bittensor.Session`, `optional`): - bittensor training session. - - """ - super(BertSynapseBase, self).__init__( - config = config, - session = session) - - # Hugging face config item. - huggingface_config = BertConfig( vocab_size=bittensor.__vocab_size__, - hidden_size=bittensor.__network_dim__, - num_hidden_layers=config.synapse.num_hidden_layers, - num_attention_heads=config.synapse.num_attention_heads, - intermediate_size=bittensor.__network_dim__, - is_decoder=False) - - # dendrite: (PKM layer) queries network using pooled embeddings as context. - # [batch_size, -1] -> topk * [batch_size, bittensor.__network_dim__] - self.dendrite = PKMDendrite(config, session, query_dim = bittensor.__network_dim__) - - # encoder_layer: encodes tokenized sequences to network dim. - # [batch_size, sequence_len] -> [batch_size, sequence_len, bittensor.__network_dim__] - self.encoder_transformer = BertModel(huggingface_config, add_pooling_layer=True) - - # context_transformer: distills the remote_context from inputs - # [batch_size, sequence_len] -> [batch_size, sequence_len, bittensor.__network_dim__] - self.context_transformer = BertModel(huggingface_config, add_pooling_layer=False) - - # hidden_layer: transforms context and encoding to network_dim hidden units. - # [batch_size, sequence_dim, 2 * bittensor.__network_dim__] -> [batch_size, sequence_len, bittensor.__network_dim__] - self.hidden_layer = torch.nn.Linear(2 * bittensor.__network_dim__, bittensor.__network_dim__) - - self.to(self.device) - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - r""" Add custom params to the parser. - """ - parser.add_argument('--synapse.num_hidden_layers', default=2, type=int, - help='Number of hidden layers in the Transformer encoder.') - parser.add_argument('--synapse.num_attention_heads', default=2, type=int, - help='Number of attention heads for each attention layer in the Transformer encoder.') - parser.add_argument('--synapse.n_block_filter', default=100, type=int, help='Stale neurons are filtered after this many blocks.') - PKMDendrite.add_args(parser) - - def forward_text(self, inputs: torch.LongTensor): - """ Local forward inputs through the BERT NSP Synapse. - - Args: - inputs (:obj:`torch.LongTensor` of shape :obj:`(batch_size, sequence_len)`, `required`): - Batch_size length list of tokenized sentences. - - Returns: - hidden (:obj:`torch.FloatTensor` of shape :obj:`(batch_size, sequence_len, bittensor.__network_dim__)`, `required`): - Hidden layer representation produced using the local_context. - """ - hidden = self.forward(inputs=inputs, remote = False).local_hidden - return hidden - - def forward(self, - inputs: torch.LongTensor, - remote: bool = False): - r""" Forward pass inputs and labels through the NSP BERT module. - - Args: - inputs (:obj:`torch.LongTensor` of shape :obj:`(batch_size, sequence_len)`, `required`): - Batch_size length list of text sentences. - - remote (:obj:`bool')`, `optional`): - Switch to True if this forward pass queries the network for the remote_context. - - bittensor.SynapseOutput ( - loss (:obj:`List[str]` of shape :obj:`(batch_size)`, `required`): - Total loss acumulation used by loss.backward() - - local_hidden (:obj:`torch.FloatTensor` of shape :obj:`(batch_size, sequence_len, bittensor.__network_dim__)`, `required`): - Hidden layer encoding produced using local_context. - - remote_hidden (:obj:`torch.FloatTensor` of shape :obj:`(batch_size, sequence_len, bittensor.__network_dim__)`, `optional`): - Hidden layer encoding produced using the remote_context. - - distillation_loss (:obj:`torch.FloatTensor` of shape :obj:`(1)`, `optional`): - Distillation loss between local_context and remote_context. - - weights (:obj:`torch.FloatTensor` of shape :obj:`(batch_size, metagraph.state.n)`, `optional`): - weights for each active neuron. - - requests_sizes (:obj:`torch.LongTensor` of shape :obj:`(metagraph.state.n)`, `optional`): - number of requests sent to each uid in this batch. - - retops (:obj:`torch.LongTensor` of shape :obj:`(metagraph.state.n)`, `optional`): - return op from each neuron. (-1 = no call, 0 = call failed, 1 = call success) - - metadata (:obj:`dict {'accuracy', torch.FloatTensor} ` of shape :obj:`(1)`, `optional`): - additional metadata output, specifically accuracy. - ) - """ - inputs = inputs.to(self.device) - # Return vars to be filled. - output = SynapseOutput(loss = torch.tensor(0.0)) - - # encoding: transformer encoded sentences. - # encoding.shape = [batch_size, sequence_len, bittensor.__network_dim__] - encoding = self.encoder_transformer(input_ids=inputs, return_dict=True) - encoding_hidden = encoding.last_hidden_state - encoding_pooled = encoding.pooler_output - - # local_context: distilled version of remote_context. - # local_context.shape = [batch_size, sequence_len, bittensor.__network_dim__] - local_context = self.context_transformer(input_ids=inputs, return_dict=True).last_hidden_state - - # local_hidden: hidden layer encoding of sequence with local_context. - # local_hidden.shape = [batch_size, sequence_len, bittensor.__network_dim__] - local_hidden = torch.cat([encoding_hidden, local_context], dim=2) - local_hidden = self.hidden_layer(local_hidden) - output.local_hidden = local_hidden - - if remote: - output = self.base_remote_forward(local_context, inputs, encoding_pooled, encoding_hidden, output) - - return output - - def base_remote_forward(self, local_context, inputs, encoding_pooled, encoding_hidden, output): - """Forward pass inputs and labels through the remote BERT networks. - - Args: - local_context (:obj: `torch.FloatTensor` of shape :obj: `(batch_size, bittensor.__network_dim__)`, `required`) - Distillation model for remote_context. - - inputs (:obj:`torch.LongTensor` of shape :obj:`(batch_size, sequence_len)`, `required`): - Batch_size length list of text sentences. - - encoding_pooled (:obj:`torch.FloatTensor` of shape (batch_size, bittensor.__network_dim__), `required`): - pooled hidden-states at the output of the last layer of the encoder. - - encoding_hidden (:obj:`torch.FloatTensor` of shape (batch_size, sequence_length, hidden_size), `optional`) : - Sequence of hidden-states at the output of the last layer of the encoder of the model. - - output (:obj: `Bittensor.SynapseOutput`, `required`) - The object containing the output thus far of the local context run - - Returns: - bittensor.SynapseOutput ( - loss (:obj:`List[str]` of shape :obj:`(batch_size)`, `required`): - Total loss acumulation used by loss.backward() - - local_hidden (:obj:`torch.FloatTensor` of shape :obj:`(batch_size, sequence_len, bittensor.__network_dim__)`, `required`): - Hidden layer encoding produced using local_context. - - remote_hidden (:obj:`torch.FloatTensor` of shape :obj:`(batch_size, sequence_len, bittensor.__network_dim__)`, `optional`): - Hidden layer encoding produced using the remote_context. - - distillation_loss (:obj:`torch.FloatTensor` of shape :obj:`(1)`, `optional`): - Distillation loss between local_context and remote_context. - - weights (:obj:`torch.FloatTensor` of shape :obj:`(batch_size, metagraph.state.n)`, `optional`): - weights for each active neuron. - - requests_sizes (:obj:`torch.LongTensor` of shape :obj:`(metagraph.state.n)`, `optional`): - number of requests sent to each uid in this batch. - - retops (:obj:`torch.LongTensor` of shape :obj:`(metagraph.state.n)`, `optional`): - return op from each neuron. (-1 = no call, 0 = call failed, 1 = call success) - - metadata (:obj:`dict {'accuracy', torch.FloatTensor} ` of shape :obj:`(1)`, `optional`): - additional metadata output, specifically accuracy. - ) - """ - # remote_context: joined responses from a bittensor.forward_text call. - # remote_context.shape = [batch_size, sequence_len, bittensor.__network_dim__] - remote_context, weights, sizes, return_codes = self.dendrite.forward_text(text = inputs, query = encoding_pooled) - output.weights = weights - output.request_sizes = sizes - output.return_codes = return_codes - remote_context = remote_context.to(self.device) - - # distillation_loss: distillation loss between local_context and remote_context - # distillation_loss.shape = [1] - distillation_loss = F.mse_loss(local_context, remote_context.detach()) - output.distillation_loss = distillation_loss - output.loss = output.loss + distillation_loss - - # remote_hidden: hidden layer encoding using remote_context. - # remote_hidden.shape = [batch_size, sequence_len, bittensor.__network_dim__] - remote_hidden = torch.cat([encoding_hidden, remote_context], dim=2) - remote_hidden = self.hidden_layer(remote_hidden) - output.remote_hidden = remote_hidden - - return output - - def remote_forward(self, output, targets): - """ - - Args: - output (bittensor.SynapseOutput): - The output object being populated by the local forward. - targets (:obj:`torch.FloatTensor` of shape :obj:`(batch_size, target_dim)`, `optional`, defaults to None): - Image labels. - - Returns: - output (bittensor.SynapseOutput ( - loss (:obj:`List[str]` of shape :obj:`(batch_size)`, `required`): - Total loss acumulation to be used by loss.backward() - - local_hidden (:obj:`torch.FloatTensor` of shape :obj:`(batch_size, bittensor.__network_dim__)`, `required`): - Hidden layer encoding produced using local_context. - - local_target (:obj:`torch.FloatTensor` of shape :obj:`(batch_size, target_dim)`, `optional`): - FFNN Target predictions using local_context. - - local_target_loss (:obj:`torch.FloatTensor` of shape :obj:`(1)`, `optional`): - FFNN Classification loss using local_context. - - remote_hidden (:obj:`torch.FloatTensor` of shape :obj:`(batch_size, bittensor.__network_dim__)`, `optional`): - Hidden layer encoding produced using the remote_context. - - remote_target (:obj:`torch.FloatTensor` of shape :obj:`(batch_size, target_dim)`, `optional`): - FFNN Target predictions using the remote_context. - - remote_target_loss (:obj:`torch.FloatTensor` of shape :obj:`(1)`, `optional`): - FFNN Classification loss using the remote_context. - - distillation_loss (:obj:`torch.FloatTensor` of shape :obj:`(1)`, `optional`): - Distillation loss between local_context and remote_context. - - weights (:obj:`torch.FloatTensor` of shape :obj:`(batch_size, metagraph.state.n)`, `required`): - weights for each active neuron. - - requests_sizes (:obj:`torch.LongTensor` of shape :obj:`(metagraph.state.n)`, `required`): - number of requests sent to each uid in this batch. - - return_codes (:obj:`List[torch.LongTensor]` of shape :obj:`[metagraph.state.n]`, `required`): - dendrite call return codes. 0 for success. - ) - """ - if targets is not None: - # remote_target: projection the local_hidden to target dimension. - # remote_target.shape = [batch_size, 2] - remote_target = self.target_layer(output.remote_hidden) - remote_target = F.softmax(remote_target, dim=1) - output.remote_target = remote_target - - return output - - -class BertNSPSynapse (BertSynapseBase): - def __init__( self, - config: Munch, - session: Session): - r""" Init a new bert nsp synapse module. - - Args: - config (:obj:`Munch`, `required`): - BertNSP configuration class. - - session (:obj:`bittensor.Session`, `required`): - bittensor session object. - """ - super(BertNSPSynapse, self).__init__( - config = config, - session = session) - - # Hugging face config item. - huggingface_config = BertConfig( vocab_size=bittensor.__vocab_size__, - hidden_size=bittensor.__network_dim__, - num_hidden_layers=config.synapse.num_hidden_layers, - num_attention_heads=config.synapse.num_attention_heads, - intermediate_size=bittensor.__network_dim__, - is_decoder=False) - - # target_layer: maps from hidden layer to vocab dimension for each token. Used by MLM loss. - # [batch_size, sequence_len, bittensor.__network_dim__] -> [batch_size, sequence_len, bittensor.__vocab_size__] - self.target_layer = transformers.modeling_bert.BertOnlyNSPHead(huggingface_config) - - # Loss function: MLM cross-entropy loss. - # predicted: [batch_size, sequence_len, 1], targets: [batch_size, sequence_len, 1] -> [1] - self.loss_fct = torch.nn.CrossEntropyLoss() - - def forward_text(self, inputs: torch.LongTensor): - """ Local forward inputs through the BERT NSP Synapse. - - Args: - inputs (:obj:`torch.LongTensor` of shape :obj:`(batch_size, sequence_len)`, `required`): - Batch_size length list of tokenized sentences. - - Returns: - hidden (:obj:`torch.FloatTensor` of shape :obj:`(batch_size, sequence_len, bittensor.__network_dim__)`, `required`): - Hidden layer representation produced using the local_context. - """ - hidden = self.forward(inputs = inputs, remote = False).local_hidden - return hidden - - - def forward(self, - inputs: torch.LongTensor, - targets: torch.Tensor = None, - remote: bool = False): - r""" Forward pass inputs and labels through the NSP BERT module. - - Args: - inputs (:obj:`torch.LongTensor` of shape :obj:`(batch_size, sequence_len)`, `required`): - Batch_size length list of text sentences. - - token_type_ids (:obj:`torch.LongTensor` of shape :obj:`(batch_size, sequence_len)`, `optional`): - Token Type IDs for training to distinguish between the sentence context and the next sentence. - - attention_mask (:obj:`torch.LongTensor` of shape :obj:`(batch_size, sequence_len)`, `optional`): - Mask to avoid performing attention on padding token indices. - Mask values selected in ``[0, 1]``: - - 1 for tokens that are **not masked**, - - 0 for tokens that are **maked**. - - targets (``torch.LongTensor`` of shape ``(batch_size,)``, `optional`): - Targets for computing the next sequence prediction (classification) loss. - Indices should be in ``[0, 1]``: - - 0 indicates sequence B is a continuation of sequence A, - - eqw1 indicates sequence B is a random sequence. - - remote (:obj:`bool')`, `optional`): - Switch to True if this forward pass queries the network for the remote_context. - - bittensor.SynapseOutput ( - loss (:obj:`List[str]` of shape :obj:`(batch_size)`, `required`): - Total loss acumulation used by loss.backward() - - local_hidden (:obj:`torch.FloatTensor` of shape :obj:`(batch_size, sequence_len, bittensor.__network_dim__)`, `required`): - Hidden layer encoding produced using local_context. - - local_target (:obj:`torch.FloatTensor` of shape :obj:`(batch_size, sequence_len, bittensor.__vocab_size__)`, `optional`): - BERT NSP Target predictions produced using local_context. - - local_target_loss (:obj:`torch.FloatTensor` of shape :obj:`(1)`, `optional`): - BERT NSP loss using local_context. - - remote_hidden (:obj:`torch.FloatTensor` of shape :obj:`(batch_size, sequence_len, bittensor.__network_dim__)`, `optional`): - Hidden layer encoding produced using the remote_context. - - remote_target (:obj:`torch.FloatTensor` of shape :obj:`(batch_size, bittensor.__vocab_size__)`, `optional`): - BERT NSP Target predictions using the remote_context. - - remote_target_loss (:obj:`torch.FloatTensor` of shape :obj:`(1)`, `optional`): - BERT NSP loss using the remote_context. - - distillation_loss (:obj:`torch.FloatTensor` of shape :obj:`(1)`, `optional`): - Distillation loss between local_context and remote_context. - - weights (:obj:`torch.FloatTensor` of shape :obj:`(batch_size, metagraph.state.n)`, `optional`): - weights for each active neuron. - - requests_sizes (:obj:`torch.LongTensor` of shape :obj:`(metagraph.state.n)`, `optional`): - number of requests sent to each uid in this batch. - - return_codes (:obj:`List[torch.LongTensor]` of shape :obj:`[metagraph.state.n]`, `required`): - dendrite call return codes. 0 for success. - ) - """ - # Call forward method from bert base. - output = BertSynapseBase.forward(self, inputs = inputs, remote = remote) - - if targets is not None: - # local_target: projection the local_hidden to target dimension. - # local_target.shape = [batch_size, 2] - local_target = self.target_layer(output.local_hidden) - local_target = F.softmax(local_target, dim=1) - output.local_target = local_target - - # local_target_loss: logit(1) > logit(0) if next_inputs are the real next sequences. - # local_target_loss: [1] - local_target_loss = self.loss_fct(local_target.view(targets.shape[0], -1), targets) - output.local_target_loss = local_target_loss - output.loss = output.loss + local_target_loss - - - if remote: - output = self.remote_forward(output, targets) - # remote_target_loss: logit(1) > logit(0) if next_inputs are the real next sequences. - # remote_target_loss: [1] - remote_target_loss = self.loss_fct(output.remote_target.view(targets.shape[0], -1), targets) - output.remote_target_loss = remote_target_loss - output.loss = output.loss + remote_target_loss - return output - - - -class BertMLMSynapse (BertSynapseBase): - def __init__( self, - config: Munch, - session: Session): - r""" Bert synapse for MLM training - - Args: - config (:obj:`Munch`, `required`): - BertNSP configuration class. - - session (:obj:`bittensor.Session`, `required`): - bittensor session object. - """ - super(BertMLMSynapse, self).__init__( - config = config, - session = session) - - # Hugging face config item. - huggingface_config = BertConfig( vocab_size=bittensor.__vocab_size__, - hidden_size=bittensor.__network_dim__, - num_hidden_layers=config.synapse.num_hidden_layers, - num_attention_heads=config.synapse.num_attention_heads, - intermediate_size=bittensor.__network_dim__, - is_decoder=False) - - # target_layer: maps from hidden layer to vocab dimension for each token. Used by MLM loss. - # [batch_size, sequence_len, bittensor.__network_dim__] -> [batch_size, sequence_len, bittensor.__vocab_size__] - self.target_layer = transformers.modeling_bert.BertLMPredictionHead(huggingface_config) - - # Loss function: MLM cross-entropy loss. - # predicted: [batch_size, sequence_len, 1], targets: [batch_size, sequence_len, 1] -> [1] - self.loss_fct = torch.nn.CrossEntropyLoss() - - def forward_text(self, inputs: torch.LongTensor): - """ Local forward inputs through the BERT NSP Synapse. - - Args: - inputs (:obj:`torch.LongTensor` of shape :obj:`(batch_size, sequence_len)`, `required`): - Batch_size length list of tokenized sentences. - - Returns: - hidden (:obj:`torch.FloatTensor` of shape :obj:`(batch_size, sequence_len, bittensor.__network_dim__)`, `required`): - Hidden layer representation produced using the local_context. - """ - hidden = self.forward(inputs = inputs, remote = False).local_hidden - return hidden - - def forward(self, - inputs: torch.LongTensor, - targets: torch.LongTensor = None, - remote: bool = False): - r""" Forward pass inputs and labels through the MLM BERT module. - - Args: - inputs (:obj:`torch.LongTensor` of shape ``(batch_size, sequence_length)``, `required`): - Batch_size length list of tokenized sentences. - - targets (:obj:`torch.LongTensor` of shape ``(batch_size, sequence_length)``, `optional`): - Targets for computing the masked language modeling loss. - Indices should be in ``[-100, 0, ..., config.vocab_size]`` (see ``input_ids`` docstring) - Tokens with indices set to ``-100`` are ignored (masked), the loss is only computed for the tokens with targets - in ``[0, ..., config.vocab_size]`` - - remote (:obj:`bool')`, `optional`): - Switch to True if this forward pass queries the network for the remote_context. - - bittensor.SynapseOutput ( - loss (:obj:`List[str]` of shape :obj:`(batch_size)`, `required`): - Total loss acumulation used by loss.backward() - - local_hidden (:obj:`torch.FloatTensor` of shape :obj:`(batch_size, sequence_len, bittensor.__network_dim__)`, `required`): - Hidden layer encoding produced using local_context. - - local_target (:obj:`torch.FloatTensor` of shape :obj:`(batch_size, sequence_len, bittensor.__vocab_size__)`, `optional`): - BERT MLM Target predictions produced using local_context. - - local_target_loss (:obj:`torch.FloatTensor` of shape :obj:`(1)`, `optional`): - BERT MLM loss using local_context. - - remote_hidden (:obj:`torch.FloatTensor` of shape :obj:`(batch_size, sequence_len, bittensor.__network_dim__)`, `optional`): - Hidden layer encoding produced using the remote_context. - - remote_target (:obj:`torch.FloatTensor` of shape :obj:`(batch_size, bittensor.__vocab_size__)`, `optional`): - BERT MLM Target predictions using the remote_context. - - remote_target_loss (:obj:`torch.FloatTensor` of shape :obj:`(1)`, `optional`): - BERT MLM loss using the remote_context. - - distillation_loss (:obj:`torch.FloatTensor` of shape :obj:`(1)`, `optional`): - Distillation loss between local_context and remote_context. - - weights (:obj:`torch.FloatTensor` of shape :obj:`(batch_size, metagraph.state.n)`, `optional`): - weights for each active neuron. - - requests_sizes (:obj:`torch.LongTensor` of shape :obj:`(metagraph.state.n)`, `optional`): - number of requests sent to each uid in this batch. - - return_codes (:obj:`List[torch.LongTensor]` of shape :obj:`[metagraph.state.n]`, `required`): - dendrite call return codes. 0 for success. - ) - """ - # Call forward method from bert base. - output = BertSynapseBase.forward(self, inputs = inputs, remote = remote) - - if targets is not None: - # local_target: projection the local_hidden to target dimension. - # local_target.shape = [batch_size, bittensor.__vocab_size__] - local_target = self.target_layer(output.local_hidden) - local_target = F.softmax(local_target, dim=1) - output.local_target = local_target - - # local_target_loss: logit(1) > logit(0) if next_inputs are the real next sequences. - # local_target_loss: [1] - local_target_loss = self.loss_fct(local_target.view(-1, bittensor.__vocab_size__), targets.view(-1)) - output.local_target_loss = local_target_loss - output.loss = output.loss + local_target_loss - - if remote: - output = self.remote_forward(output, targets) - # remote_target_loss: logit(1) > logit(0) if next_inputs are the real next sequences. - # remote_target_loss: [1] - remote_target_loss = self.loss_fct(output.remote_target.view(-1, bittensor.__vocab_size__), targets.view(-1)) - output.remote_target_loss = remote_target_loss - output.loss = output.loss + remote_target_loss - - return output diff --git a/bittensor/synapses/gpt2.py b/bittensor/synapses/gpt2.py deleted file mode 100644 index 73169bd2fa..0000000000 --- a/bittensor/synapses/gpt2.py +++ /dev/null @@ -1,363 +0,0 @@ -import bittensor -from bittensor.dendrites.pkm import PKMDendrite -from bittensor.synapse import Synapse -from bittensor.synapse import SynapseOutput -from bittensor.session import Session - -import argparse -from munch import Munch -import random -import torch -from torch import nn -import torch.nn.functional as F -from transformers import GPT2Config, GPT2Model - -def nextbatch(data, batch_size, tokenizer): - """ Returns a random batch of sentences from text dataset. - - Args: - data: (List[dict{'text': str}]): Dataset of text inputs. - batch_size: size of batch to create. - - Returns: - batch_inputs torch.Tensor (batch_size, sequence_length): List of tokenized sentences. - """ - batch_text = [] - for _ in range(batch_size): - text = data[random.randint(0, len(data))]['text'] - batch_text.append(text) - batch_inputs = tokenizer.encode(batch_text, return_tensors='pt', padding=True, max_length=bittensor.__max_sequence_length__) - return batch_inputs - -class GPT2Pooler(nn.Module): - - def __init__(self, config): - super().__init__() - self.dense = nn.Linear(config.n_embd, config.n_embd) - self.activation = nn.Tanh() - - def forward(self, hidden_states): - # We "pool" the model by simply taking the hidden state corresponding - # to the first token. - first_token_tensor = hidden_states[:, 0] - pooled_output = self.dense(first_token_tensor) - pooled_output = self.activation(pooled_output) - return pooled_output - -class GPT2LMSynapse(Synapse): - """ A Bittensor Synapse training GPT2 with Masked Language Modelling (MLM) - """ - - def __init__(self, - config: Munch, - session: Session): - r""" Init a new ffnn synapse module. - - Args: - config (:obj:`munch.Munch`, `required`): - munched config class. - - session (:obj:`bittensor.Session`, `required`): - bittensor session object. - Defaults to bittensor.session global if exists. - """ - super(GPT2LMSynapse, self).__init__( - config = config, - session = session) - - # Build hugging face config. - huggingface_config = GPT2Config( - vocab_size=bittensor.__vocab_size__, - n_embd=bittensor.__network_dim__, - n_layer=config.synapse.n_layer, - n_head=config.synapse.n_head, - n_inner=config.synapse.n_inner, - activation_function=config.synapse.activation_function, - resid_pdrop=config.synapse.resid_pdrop, - embd_pdrop=config.synapse.embd_pdrop, - attn_pdrop=config.synapse.attn_pdrop, - layer_norm_epsilon=config.synapse.layer_norm_epsilon, - initializer_range=config.synapse.initializer_range, - summary_type=config.synapse.summary_type, - summary_use_proj=config.synapse.summary_use_proj, - summary_activation=config.synapse.summary_activation, - summary_proj_to_labels=config.synapse.summary_proj_to_labels, - summary_first_dropout=config.synapse.summary_first_dropout, - bos_token_id=50256, - eos_token_id=50256 - ) - - # encoder_layer: encodes tokenized sequences to network dim. - # [batch_size, sequence_len] -> [batch_size, sequence_len, bittensor.__network_dim__] - self.encoder_transformer = GPT2Model(huggingface_config) - - # pooler_layer: pools transformed sequence to network_dim for router. - # [batch_size, bittensor.__network_dim__, sequence_len] -> [batch_size, bittensor.__network_dim__] - self.pooler = GPT2Pooler(huggingface_config) - - # dendrite: (PKM layer) queries network using pooled embeddings as context. - # [batch_size, bittensor.__network_dim__] -> topk * [batch_size, bittensor.__network_dim__] - self.dendrite = PKMDendrite(config, session, query_dim = bittensor.__network_dim__) - - # context_transformer: distills the remote_context from inputs - # [batch_size, sequence_len] -> [batch_size, sequence_len, bittensor.__network_dim__] - self.context_transformer = GPT2Model(huggingface_config) - - # hidden_layer: transforms context and encoding to network_dim hidden units. - # [batch_size, sequence_dim, 2 * bittensor.__network_dim__] -> [batch_size, sequence_len, bittensor.__network_dim__] - self.hidden_layer = torch.nn.Linear(2 * bittensor.__network_dim__, bittensor.__network_dim__) - - # target_layer: maps from hidden layer to vocab dimension for each token. Used by MLM loss. - # [batch_size, sequence_len, bittensor.__network_dim__] -> [batch_size, sequence_len, bittensor.__vocab_size__] - self.target_layer = nn.Linear(bittensor.__network_dim__, bittensor.__vocab_size__, bias=False) - - # Loss function: MLM cross-entropy loss. - # predicted: [batch_size, sequence_len, 1], targets: [batch_size, sequence_len, 1] -> [1] - self.loss_fct = torch.nn.CrossEntropyLoss() - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - r""" Add custom params to the parser. - """ - parser.add_argument('--synapse.n_head', default=2, type=int, - help='Number of attention heads for each attention layer in the Transformer encoder.') - parser.add_argument('--synapse.n_layer', default=12, type=int, - help='Number of hidden layers in the Transformer encoder.') - parser.add_argument('--synapse.n_inner', default=None, type=int, - help='The dimensionality of the inner feed-forward layers. :obj:`None` will set it to 4 times n_embd') - parser.add_argument('--synapse.activation_function', default='gelu_new', type=str, - help='Activation function, to be selected in the list :obj:`["relu", "silu", "gelu", "tanh", "gelu_new"]') - parser.add_argument('--synapse.resid_pdrop', default=0.1, type=float, - help='GPT residual dropout probabilit.') - parser.add_argument('--synapse.embd_pdrop', default=0.1, type=float, - help='GPT embedding dropout probability.') - parser.add_argument('--synapse.attn_pdrop', default=0.1, type=float, - help='GPT attention dropout probability.') - parser.add_argument('--synapse.layer_norm_epsilon', default=1e-05, type=float, - help='GPT the epsilon to use in the layer normalization layers') - parser.add_argument('--synapse.summary_type', default='cls_index', type=str, - help='Supply a Tensor of classification token position (like GPT/GPT-2).') - parser.add_argument('--synapse.initializer_range', default=0.02, type=float, - help='The standard deviation of the truncated_normal_initializer for initializing all weight matrices.') - parser.add_argument('--synapse.summary_use_proj', default=True, type=bool, - help='Whether or not to add a projection after the vector extraction.') - parser.add_argument('--synapse.summary_activation', type=str, - help='Pass "tanh" for a tanh activation to the output, any other value will result in no activation.') - parser.add_argument('--synapse.summary_proj_to_labels', default=True, type=bool, - help='Whether the projection outputs should have config.num_labels or config.hidden_size classes.') - parser.add_argument('--synapse.summary_first_dropout', default=0.1, type=float, - help='The dropout ratio to be used after the projection and activation.') - parser.add_argument('--synapse.n_block_filter', default=100, type=int, help='Stale neurons are filtered after this many blocks.') - PKMDendrite.add_args(parser) - - def forward_text(self, inputs: torch.LongTensor): - """ Local forward inputs through the MLM GPT Synapse. - - Args: - inputs (:obj:`torch.LongTensor` of shape :obj:`(batch_size, sequence_len)`, `required`): - Batch_size length list of tokenized sentences. - - Returns: - hidden (:obj:`torch.FloatTensor` of shape :obj:`(batch_size, sequence_len, bittensor.__network_dim__)`, `required`): - Hidden layer representation produced using the local_context. - """ - hidden = self.forward(inputs=inputs, training = False, remote = False).local_hidden - return hidden - - def forward(self, - inputs: torch.LongTensor, - training: bool = True, - remote: bool = False): - r""" Forward pass through GPT MLM synapse. - - Args: - inputs (:obj:`torch.LongTensor` of shape :obj:`(batch_size, sequence_len)`, `required`): - Batch_size length list of text sentences. - - training (:obj:`bool')`, `optional`, defaults to True): - Switch to True if this forward pass computes an MLM loss. - - remote (:obj:`bool')`, `optional`): - Switch to True if this forward pass queries the network for the remote_context. - - bittensor.SynapseOutput ( - loss (:obj:`List[str]` of shape :obj:`(batch_size)`, `required`): - Total loss acumulation used by loss.backward() - - local_hidden (:obj:`torch.FloatTensor` of shape :obj:`(batch_size, sequence_len, bittensor.__network_dim__)`, `required`): - Hidden layer encoding produced using local_context. - - local_target (:obj:`torch.FloatTensor` of shape :obj:`(batch_size, sequence_len, bittensor.__vocab_size__)`, `optional`): - GPT MLM Target predictions produced using local_context. - - local_target_loss (:obj:`torch.FloatTensor` of shape :obj:`(1)`, `optional`): - GPT MLM loss using local_context. - - remote_hidden (:obj:`torch.FloatTensor` of shape :obj:`(batch_size, sequence_len, bittensor.__network_dim__)`, `optional`): - Hidden layer encoding produced using the remote_context. - - remote_target (:obj:`torch.FloatTensor` of shape :obj:`(batch_size, bittensor.__vocab_size__)`, `optional`): - GPT MLM Target predictions using the remote_context. - - remote_target_loss (:obj:`torch.FloatTensor` of shape :obj:`(1)`, `optional`): - GPT MLM loss using the remote_context. - - distillation_loss (:obj:`torch.FloatTensor` of shape :obj:`(1)`, `optional`): - Distillation loss between local_context and remote_context. - - weights (:obj:`torch.FloatTensor` of shape :obj:`(batch_size, metagraph.state.n)`, `optional`): - weights for each active neuron. - - requests_sizes (:obj:`torch.LongTensor` of shape :obj:`(metagraph.state.n)`, `optional`): - number of requests sent to each uid in this batch. - - return_codes (:obj:`List[torch.LongTensor]` of shape :obj:`[metagraph.state.n]`, `required`): - dendrite call return codes. 0 for success. - - metadata (:obj:`dict {'accuracy', torch.FloatTensor} ` of shape :obj:`(1)`, `optional`): - additional metadata output, specifically accuracy. - ) - """ - - # Return vars to be filled. - output = SynapseOutput(loss = torch.tensor(0.0)) - - # encoding: transformer encoded sentences. - # encoding.shape = [batch_size, sequence_len, bittensor.__network_dim__] - encoding = self.encoder_transformer(input_ids=inputs, return_dict=True).last_hidden_state - - # pooled: pooled encodings by taking the hidden units of the last token. - # pooled.shape = [batch_size, bittensor.__network_dim__] - pooled = self.pooler(encoding) - - # local_context: distilled version of remote_context. - # local_context.shape = [batch_size, sequence_len, bittensor.__network_dim__] - local_context = self.context_transformer(input_ids=inputs, return_dict=True).last_hidden_state - - # local_hidden: hidden layer encoding of sequence with local_context. - # local_hidden.shape = [batch_size, sequence_len, bittensor.__network_dim__] - local_hidden = torch.cat([encoding, local_context], dim=2) - local_hidden = self.hidden_layer(local_hidden) - output.local_hidden = local_hidden - - if training: - # local_target: projection of local_hidden onto target dimension. - # local_target.shape = [batch_size, sequence_len, bittensor.__vocab_size__] - local_target = self.target_layer(local_hidden) - output.local_target = local_target - - # local_target_loss: MLM loss between local_target and passed targets. - # local_target_loss.shape = [1] - shift_logits = local_target[..., :-1, :].contiguous() - shift_labels = inputs[..., 1:].contiguous() - local_target_loss = self.loss_fct( - shift_logits.view(-1, shift_logits.size(-1)), shift_labels.view(-1)) - output.loss = output.loss + local_target_loss - output.local_target_loss = local_target_loss - - if remote: - output = self.forward_remote(local_context, local_hidden, inputs, pooled, encoding, training, output) - - return output - - def forward_remote(self, local_context, local_hidden, inputs, pooled, encoding, training, output): - """ Forward pass inputs and labels through the GPT2 module. - - - Args: - local_context (:obj: `torch.FloatTensor` of shape :obj: `(batch_size, sequence_len, bittensor.__network_dim__)`, `required`) - Distillation model for remote_context. - - local_hidden (:obj:`torch.FloatTensor` of shape :obj:`(batch_size, sequence_len, bittensor.__network_dim__)`, `required`): - Hidden layer encoding produced using local_context. - - - inputs (:obj:`torch.LongTensor` of shape :obj:`(batch_size, sequence_len)`, `required`): - Batch_size length list of tokenized sentences. - - encoding (:obj:`torch.LongTensor` of shape :obj:`` - Transformer encoded sentences - - training (:obj:`bool')`, `optional`, defaults to True): - Switch to True if this forward pass computes an MLM loss. - - output (SynapseOutput): Object being populated by local forward. - - Returns: - bittensor.SynapseOutput ( - loss (:obj:`List[str]` of shape :obj:`(batch_size)`, `required`): - Total loss acumulation used by loss.backward() - - local_hidden (:obj:`torch.FloatTensor` of shape :obj:`(batch_size, sequence_len, bittensor.__network_dim__)`, `required`): - Hidden layer encoding produced using local_context. - - local_target (:obj:`torch.FloatTensor` of shape :obj:`(batch_size, sequence_len, bittensor.__vocab_size__)`, `optional`): - BERT NSP Target predictions produced using local_context. - - local_target_loss (:obj:`torch.FloatTensor` of shape :obj:`(1)`, `optional`): - BERT NSP loss using local_context. - - remote_hidden (:obj:`torch.FloatTensor` of shape :obj:`(batch_size, sequence_len, bittensor.__network_dim__)`, `optional`): - Hidden layer encoding produced using the remote_context. - - remote_target (:obj:`torch.FloatTensor` of shape :obj:`(batch_size, bittensor.__vocab_size__)`, `optional`): - BERT NSP Target predictions using the remote_context. - - remote_target_loss (:obj:`torch.FloatTensor` of shape :obj:`(1)`, `optional`): - BERT NSP loss using the remote_context. - - distillation_loss (:obj:`torch.FloatTensor` of shape :obj:`(1)`, `optional`): - Distillation loss between local_context and remote_context. - - weights (:obj:`torch.FloatTensor` of shape :obj:`(batch_size, metagraph.state.n)`, `optional`): - weights for each active neuron. - - requests_sizes (:obj:`torch.LongTensor` of shape :obj:`(metagraph.state.n)`, `optional`): - number of requests sent to each uid in this batch. - - return_codes (:obj:`List[torch.LongTensor]` of shape :obj:`[num_neurons]`, `required`): - dendrite call return codes. 0 for success. - - metadata (:obj:`dict {'accuracy', torch.FloatTensor} ` of shape :obj:`(1)`, `optional`): - additional metadata output, specifically accuracy. - ) - """ - # remote_context: joined responses from a bittensor.forward_text call. - # remote_context.shape = [batch_size, sequence_len, bittensor.__network_dim__] - remote_context, weights, sizes, return_codes = self.dendrite.forward_text(inputs, pooled) - output.weights = weights - output.return_codes = return_codes - output.request_sizes = sizes - - # distillation_loss: distillation loss between local_context and remote_context - # distillation_loss.shape = [1] - distillation_loss = F.mse_loss(local_context, remote_context.detach()) - output.loss = output.loss + distillation_loss - output.distillation_loss = distillation_loss - - # remote_hidden: hidden layer encoding using remote_context. - # remote_hidden.shape = [batch_size, sequence_len, bittensor.__network_dim__] - remote_hidden = torch.cat([encoding, remote_context], dim=2) - remote_hidden = self.hidden_layer(remote_hidden) - - if training: - # remote_target: projection of remote_hidden onto target dimension. - # remote_target.shape = [batch_size, sequence_len, bittensor.__vocab_size__] - remote_target = self.target_layer(local_hidden) - output.remote_target = remote_target - - # remote_target_loss: MLM loss between remote_target and passed targets. - # remote_target_loss.shape = [1] - shift_logits = remote_target[..., :-1, :].contiguous() - - shift_labels = inputs[..., 1:].contiguous() - remote_target_loss = self.loss_fct( - shift_logits.view(-1, shift_logits.size(-1)), shift_labels.view(-1)) - - output.loss = output.loss + remote_target_loss - output.remote_target_loss = remote_target_loss - - - return output - - diff --git a/bittensor/utils/__init__.py b/bittensor/utils/__init__.py index 00f01e7c62..da2ea4fbaa 100644 --- a/bittensor/utils/__init__.py +++ b/bittensor/utils/__init__.py @@ -1,6 +1,486 @@ -import getpass +import ast +import decimal +import hashlib +import warnings +from collections import namedtuple +from typing import Any, Literal, Union, Optional, TYPE_CHECKING +from urllib.parse import urlparse +import inspect +import scalecodec +from async_substrate_interface.utils import ( + hex_to_bytes, +) +from bittensor_wallet import Keypair +from bittensor_wallet.errors import KeyFileError, PasswordError +from scalecodec import ss58_decode, is_valid_ss58_address as _is_valid_ss58_address -class Cli: - @staticmethod - def ask_password(): - return getpass.getpass("Enter password to unlock key: ") +from bittensor.core import settings +from bittensor.core.settings import SS58_FORMAT +from bittensor.utils.btlogging import logging +from .registration import torch, use_torch +from .version import version_checking, check_version, VersionCheckError + +if TYPE_CHECKING: + from bittensor_wallet import Wallet + from bittensor.utils.balance import Balance + +BT_DOCS_LINK = "https://docs.bittensor.com" + + +# redundant aliases +logging = logging +torch = torch +use_torch = use_torch +version_checking = version_checking +check_version = check_version +VersionCheckError = VersionCheckError +ss58_decode = ss58_decode +hex_to_bytes = hex_to_bytes + + +RAOPERTAO = 1e9 +U16_MAX = 65535 +U64_MAX = 18446744073709551615 + +UnlockStatus = namedtuple("UnlockStatus", ["success", "message"]) + + +class Certificate(str): + def __new__(cls, data: Union[str, dict]): + if isinstance(data, dict): + tuple_ascii = data["public_key"][0] + string = chr(data["algorithm"]) + "".join(chr(i) for i in tuple_ascii) + else: + string = data + return str.__new__(cls, string) + + +def decode_hex_identity_dict(info_dictionary: dict[str, Any]) -> 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}") + else: + decoded_info[k] = item + return decoded_info + + +def ss58_to_vec_u8(ss58_address: str) -> list[int]: + ss58_bytes: bytes = ss58_address_to_bytes(ss58_address) + encoded_address: list[int] = [int(byte) for byte in ss58_bytes] + return encoded_address + + +def strtobool(val: str) -> Union[bool, Literal["==SUPRESS=="]]: + """ + Converts a string to a boolean value. + + truth-y values are 'y', 'yes', 't', 'true', 'on', and '1'; + false-y values are 'n', 'no', 'f', 'false', 'off', and '0'. + + Raises ValueError if 'val' is anything else. + """ + val = val.lower() + if val in ("y", "yes", "t", "true", "on", "1"): + return True + elif val in ("n", "no", "f", "false", "off", "0"): + return False + else: + raise ValueError("invalid truth value %r" % (val,)) + + +def _get_explorer_root_url_by_network_from_map( + network: str, network_map: dict[str, dict[str, str]] +) -> Optional[dict[str, str]]: + """ + Returns the explorer root url for the given network name from the given network map. + + Args: + network(str): The network to get the explorer url for. + network_map(dict[str, str]): The network map to get the explorer url from. + + Returns: + The explorer url for the given network. + Or None if the network is not in the network map. + """ + explorer_urls: Optional[dict[str, str]] = {} + for entity_nm, entity_network_map in network_map.items(): + if network in entity_network_map: + explorer_urls[entity_nm] = entity_network_map[network] + + return explorer_urls + + +def get_explorer_url_for_network( + network: str, block_hash: str, network_map: dict[str, dict[str, str]] +) -> Optional[dict[str, str]]: + """ + Returns the explorer url for the given block hash and network. + + Args: + network(str): The network to get the explorer url for. + block_hash(str): The block hash to get the explorer url for. + network_map(dict[str, dict[str, str]]): The network maps to get the explorer urls from. + + Returns: + The explorer url for the given block hash and network. + Or None if the network is not known. + """ + + explorer_urls: Optional[dict[str, str]] = {} + # Will be None if the network is not known. i.e. not in network_map + explorer_root_urls: Optional[dict[str, str]] = ( + _get_explorer_root_url_by_network_from_map(network, network_map) + ) + + if explorer_root_urls != {}: + # We are on a known network. + explorer_opentensor_url = ( + f"{explorer_root_urls.get('opentensor')}/query/{block_hash}" + ) + explorer_taostats_url = ( + f"{explorer_root_urls.get('taostats')}/extrinsic/{block_hash}" + ) + explorer_urls["opentensor"] = explorer_opentensor_url + explorer_urls["taostats"] = explorer_taostats_url + + return explorer_urls + + +def ss58_address_to_bytes(ss58_address: str) -> bytes: + """Converts a ss58 address to a bytes object.""" + account_id_hex: str = scalecodec.ss58_decode(ss58_address, SS58_FORMAT) + return bytes.fromhex(account_id_hex) + + +def u16_normalized_float(x: int) -> float: + return float(x) / float(U16_MAX) + + +def u64_normalized_float(x: int) -> float: + return float(x) / float(U64_MAX) + + +def float_to_u64(value: float) -> int: + """Converts a float to a u64 int""" + + value = decimal.Decimal(str(value)) + + if not (0 <= value <= 1): + raise ValueError("Input value must be between 0 and 1") + + return int(value * U64_MAX) + + +def get_hash(content, encoding="utf-8"): + sha3 = hashlib.sha3_256() + + # Update the hash object with the concatenated string + sha3.update(content.encode(encoding)) + + # Produce the hash + return sha3.hexdigest() + + +def format_error_message(error_message: Union[dict, Exception]) -> str: + """ + Formats an error message from the Subtensor error information for use in extrinsics. + + Args: + error_message: A dictionary containing the error information from Subtensor, or a SubstrateRequestException + containing dictionary literal args. + + Returns: + str: A formatted error message string. + """ + err_name = "UnknownError" + err_type = "UnknownType" + err_description = "Unknown Description" + + if isinstance(error_message, Exception): + # generally gotten through SubstrateRequestException args + new_error_message = None + for arg in error_message.args: + try: + d = ast.literal_eval(arg) + if isinstance(d, dict): + if "error" in d: + new_error_message = d["error"] + break + elif all(x in d for x in ["code", "message", "data"]): + new_error_message = d + break + except (ValueError, TypeError, SyntaxError, MemoryError, RecursionError): + pass + if new_error_message is None: + return_val = " ".join(error_message.args) + + return f"Subtensor returned: {return_val}" + else: + error_message = new_error_message + + if isinstance(error_message, dict): + # subtensor error structure + if ( + error_message.get("code") + and error_message.get("message") + and error_message.get("data") + ): + err_name = "SubstrateRequestException" + err_type = error_message.get("message", "") + err_data = error_message.get("data", "") + + # subtensor custom error marker + if err_data.startswith("Custom error:"): + err_description = ( + f"{err_data} | Please consult {BT_DOCS_LINK}/errors/custom" + ) + else: + err_description = err_data + + elif ( + error_message.get("type") + and error_message.get("name") + and error_message.get("docs") + ): + err_type = error_message.get("type", err_type) + err_name = error_message.get("name", err_name) + err_docs = error_message.get("docs", [err_description]) + err_description = ( + err_docs if isinstance(err_docs, str) else " ".join(err_docs) + ) + err_description += ( + f" | Please consult {BT_DOCS_LINK}/errors/subtensor#{err_name.lower()}" + ) + + elif error_message.get("code") and error_message.get("message"): + err_type = error_message.get("code", err_name) + err_name = "Custom type" + err_description = error_message.get("message", err_description) + + else: + logging.error( + f"String representation of real error_message: {str(error_message)}" + ) + + return f"Subtensor returned `{err_name}({err_type})` error. This means: `{err_description}`." + + +def is_valid_ss58_address(address: str) -> bool: + """ + Checks if the given address is a valid ss58 address. + + Args: + address(str): The address to check. + + Returns: + True if the address is a valid ss58 address for Bittensor, False otherwise. + """ + try: + return _is_valid_ss58_address( + address, valid_ss58_format=SS58_FORMAT + ) or _is_valid_ss58_address( + address, valid_ss58_format=42 + ) # Default substrate ss58 format (legacy) + except IndexError: + return False + + +def _is_valid_ed25519_pubkey(public_key: Union[str, bytes]) -> bool: + """ + Checks if the given public_key is a valid ed25519 key. + + Args: + public_key(Union[str, bytes]): The public_key to check. + + Returns: + True if the public_key is a valid ed25519 key, False otherwise. + + """ + try: + if isinstance(public_key, str): + if len(public_key) != 64 and len(public_key) != 66: + raise ValueError("a public_key should be 64 or 66 characters") + elif isinstance(public_key, bytes): + if len(public_key) != 32: + raise ValueError("a public_key should be 32 bytes") + else: + raise ValueError("public_key must be a string or bytes") + + keypair = Keypair(public_key=public_key) + + ss58_addr = keypair.ss58_address + return ss58_addr is not None + + except (ValueError, IndexError): + return False + + +def is_valid_bittensor_address_or_public_key(address: Union[str, bytes]) -> bool: + """ + Checks if the given address is a valid destination address. + + Args: + address(Union[str, bytes]): The address to check. + + Returns: + True if the address is a valid destination address, False otherwise. + """ + if isinstance(address, str): + # Check if ed25519 + if address.startswith("0x"): + return _is_valid_ed25519_pubkey(address) + else: + # Assume ss58 address + return is_valid_ss58_address(address) + elif isinstance(address, bytes): + # Check if ed25519 + return _is_valid_ed25519_pubkey(address) + else: + # Invalid address type + return False + + +def validate_chain_endpoint(endpoint_url: str) -> tuple[bool, str]: + """Validates if the provided endpoint URL is a valid WebSocket URL.""" + parsed = urlparse(endpoint_url) + if parsed.scheme not in ("ws", "wss"): + return False, ( + f"Invalid URL or network name provided: ({endpoint_url}).\n" + "Allowed network names are finney, test, local. " + "Valid chain endpoints should use the scheme `ws` or `wss`.\n" + ) + if not parsed.netloc: + return False, "Invalid URL passed as the endpoint" + return True, "" + + +def unlock_key( + wallet: "Wallet", + unlock_type="coldkey", + raise_error=False, +) -> "UnlockStatus": + """ + Attempts to decrypt a wallet's coldkey or hotkey + + Args: + wallet: a Wallet object + unlock_type: the key type, 'coldkey' or 'hotkey' + raise_error: if False, will return (False, error msg), if True will raise the otherwise-caught exception. + + Returns: + UnlockStatus for success status of unlock, with error message if unsuccessful + + Raises: + bittensor_wallet.errors.PasswordError: incorrect password + bittensor_wallet.errors.KeyFileError: keyfile is corrupt, non-writable, or non-readable, or non-existent + """ + if unlock_type == "coldkey": + unlocker = "unlock_coldkey" + elif unlock_type == "hotkey": + unlocker = "unlock_hotkey" + else: + raise ValueError( + f"Invalid unlock type provided: {unlock_type}. Must be 'coldkey' or 'hotkey'." + ) + try: + getattr(wallet, unlocker)() + return UnlockStatus(True, "") + except PasswordError: + if raise_error: + raise + + err_msg = f"The password used to decrypt your {unlock_type.capitalize()} keyfile is invalid." + return UnlockStatus(False, err_msg) + except KeyFileError: + if raise_error: + raise + + err_msg = f"{unlock_type.capitalize()} keyfile is corrupt, non-writable, or non-readable, or non-existent." + return UnlockStatus(False, err_msg) + + +def determine_chain_endpoint_and_network( + network: str, +) -> tuple[Optional[str], Optional[str]]: + """Determines the chain endpoint and network from the passed network or chain_endpoint. + + Arguments: + network (str): The network flag. The choices are: ``finney`` (main network), ``archive`` (archive network + +300 blocks), ``local`` (local running network), ``test`` (test network). + + Returns: + tuple[Optional[str], Optional[str]]: The network and chain endpoint flag. If passed, overrides the + ``network`` argument. + """ + + if network is None: + return None, None + if network in settings.NETWORKS: + return network, settings.NETWORK_MAP[network] + + substrings_map = { + "entrypoint-finney.opentensor.ai": ("finney", settings.FINNEY_ENTRYPOINT), + "test.finney.opentensor.ai": ("test", settings.FINNEY_TEST_ENTRYPOINT), + "archive.chain.opentensor.ai": ("archive", settings.ARCHIVE_ENTRYPOINT), + "subvortex": ("subvortex", settings.SUBVORTEX_ENTRYPOINT), + "127.0.0.1": ("local", network), + "localhost": ("local", network), + } + + for substring, result in substrings_map.items(): + if substring in network and validate_chain_endpoint(network): + return result + + return "unknown", network + + +def deprecated_message(message: str) -> None: + """Shows a deprecation warning message with the given message.""" + warnings.simplefilter("default", DeprecationWarning) + warnings.warn(message=message, category=DeprecationWarning, stacklevel=2) + + +def get_transfer_fn_params( + amount: Optional["Balance"], destination: str, keep_alive: bool +) -> tuple[str, dict[str, Union[str, int, bool]]]: + """ + Helper function to get the transfer call function and call params, depending on the value and keep_alive flag + provided + + Args: + amount: the amount of Tao to transfer. `None` if transferring all. + destination: the destination SS58 of the transfer + keep_alive: whether to enforce a retention of the existential deposit in the account after transfer. + + Returns: + tuple[call function, call params] + """ + call_params = {"dest": destination} + if amount is None: + call_function = "transfer_all" + if keep_alive: + call_params["keep_alive"] = True + else: + call_params["keep_alive"] = False + else: + call_params["value"] = amount.rao + if keep_alive: + call_function = "transfer_keep_alive" + else: + call_function = "transfer_allow_death" + return call_function, call_params + + +def get_function_name() -> str: + """Returns the name of the calling function.""" + return inspect.currentframe().f_back.f_code.co_name diff --git a/bittensor/utils/__pycache__/__init__.cpython-38.pyc b/bittensor/utils/__pycache__/__init__.cpython-38.pyc deleted file mode 100644 index 6f7d32fea1..0000000000 Binary files a/bittensor/utils/__pycache__/__init__.cpython-38.pyc and /dev/null differ diff --git a/bittensor/utils/__pycache__/asyncio.cpython-38.pyc b/bittensor/utils/__pycache__/asyncio.cpython-38.pyc deleted file mode 100644 index 7cb8005acd..0000000000 Binary files a/bittensor/utils/__pycache__/asyncio.cpython-38.pyc and /dev/null differ diff --git a/bittensor/utils/__pycache__/batch_transforms.cpython-38.pyc b/bittensor/utils/__pycache__/batch_transforms.cpython-38.pyc deleted file mode 100644 index dc35d4ee15..0000000000 Binary files a/bittensor/utils/__pycache__/batch_transforms.cpython-38.pyc and /dev/null differ diff --git a/bittensor/utils/__pycache__/dispatcher.cpython-38.pyc b/bittensor/utils/__pycache__/dispatcher.cpython-38.pyc deleted file mode 100644 index 1c1fec65f7..0000000000 Binary files a/bittensor/utils/__pycache__/dispatcher.cpython-38.pyc and /dev/null differ diff --git a/bittensor/utils/__pycache__/gate.cpython-38.pyc b/bittensor/utils/__pycache__/gate.cpython-38.pyc deleted file mode 100644 index 98ff8db2d2..0000000000 Binary files a/bittensor/utils/__pycache__/gate.cpython-38.pyc and /dev/null differ diff --git a/bittensor/utils/__pycache__/keys.cpython-38.pyc b/bittensor/utils/__pycache__/keys.cpython-38.pyc deleted file mode 100644 index 62afc800ef..0000000000 Binary files a/bittensor/utils/__pycache__/keys.cpython-38.pyc and /dev/null differ diff --git a/bittensor/utils/__pycache__/logging.cpython-38.pyc b/bittensor/utils/__pycache__/logging.cpython-38.pyc deleted file mode 100644 index 7afbd8a63c..0000000000 Binary files a/bittensor/utils/__pycache__/logging.cpython-38.pyc and /dev/null differ diff --git a/bittensor/utils/__pycache__/ptp.cpython-38.pyc b/bittensor/utils/__pycache__/ptp.cpython-38.pyc deleted file mode 100644 index ae41585462..0000000000 Binary files a/bittensor/utils/__pycache__/ptp.cpython-38.pyc and /dev/null differ diff --git a/bittensor/utils/__pycache__/replicate_utils.cpython-38.pyc b/bittensor/utils/__pycache__/replicate_utils.cpython-38.pyc deleted file mode 100644 index c0a8293093..0000000000 Binary files a/bittensor/utils/__pycache__/replicate_utils.cpython-38.pyc and /dev/null differ diff --git a/bittensor/utils/__pycache__/router.cpython-38.pyc b/bittensor/utils/__pycache__/router.cpython-38.pyc deleted file mode 100644 index 57f87c7f47..0000000000 Binary files a/bittensor/utils/__pycache__/router.cpython-38.pyc and /dev/null differ diff --git a/bittensor/utils/asyncio.py b/bittensor/utils/asyncio.py deleted file mode 100644 index 5f95f892da..0000000000 --- a/bittensor/utils/asyncio.py +++ /dev/null @@ -1,38 +0,0 @@ -import asyncio -import concurrent - -class Asyncio: - loop = None - - @staticmethod - def init(): - Asyncio.loop = asyncio.get_event_loop() - - @staticmethod - def add_task(method): - loop = asyncio.get_event_loop() - loop.create_task(method) - - @staticmethod - def stop(): - loop = asyncio.get_event_loop() - loop.stop() - - @staticmethod - def start_in_thread(method, args): - executor = concurrent.futures.ThreadPoolExecutor( - max_workers=3, - ) - - Asyncio.loop.run_in_executor(executor, method, args) - - @staticmethod - def run_forever(): - Asyncio.loop.run_forever() - - @staticmethod - def run_in_new_loop(task): - loop = asyncio.new_event_loop() - result = loop.run_until_complete(task) - loop.stop() - return result diff --git a/bittensor/utils/axon_utils.py b/bittensor/utils/axon_utils.py new file mode 100644 index 0000000000..b24f4b6be7 --- /dev/null +++ b/bittensor/utils/axon_utils.py @@ -0,0 +1,43 @@ +from typing import Optional + +ALLOWED_DELTA = 4_000_000_000 # Delta of 4 seconds for nonce validation +NANOSECONDS_IN_SECOND = 1_000_000_000 + + +def allowed_nonce_window_ns( + current_time_ns: int, synapse_timeout: Optional[float] = None +) -> int: + """ + Calculates the allowed window for a nonce in nanoseconds. + + Args: + current_time_ns (int): The current time in nanoseconds. + synapse_timeout (Optional[float]): The optional timeout for the synapse in seconds. If None, it defaults to 0. + + Returns: + int: The allowed nonce window in nanoseconds. + """ + synapse_timeout_ns = (synapse_timeout or 0) * NANOSECONDS_IN_SECOND + allowed_window_ns = current_time_ns - ALLOWED_DELTA - synapse_timeout_ns + return allowed_window_ns + + +def calculate_diff_seconds( + current_time: int, synapse_timeout: Optional[float], synapse_nonce: int +): + """ + Calculates the difference in seconds between the current time and the synapse nonce, + and also returns the allowed delta in seconds. + + Args: + current_time (int): The current time in nanoseconds. + synapse_timeout (Optional[float]): The optional timeout for the synapse in seconds. + synapse_nonce (int): The nonce value for the synapse in nanoseconds. + + Returns: + tuple: A tuple containing the difference in seconds (float) and the allowed delta in seconds (float). + """ + synapse_timeout_ns = (synapse_timeout or 0) * NANOSECONDS_IN_SECOND + diff_seconds = (current_time - synapse_nonce) / NANOSECONDS_IN_SECOND + allowed_delta_seconds = (ALLOWED_DELTA + synapse_timeout_ns) / NANOSECONDS_IN_SECOND + return diff_seconds, allowed_delta_seconds diff --git a/bittensor/utils/balance.py b/bittensor/utils/balance.py new file mode 100644 index 0000000000..99c1a85831 --- /dev/null +++ b/bittensor/utils/balance.py @@ -0,0 +1,838 @@ +import warnings +from typing import Union, TypedDict, Optional + +from scalecodec import ScaleType + +from bittensor.core import settings +from bittensor.utils import deprecated_message + + +def _check_currencies(self, other): + """Checks that Balance objects have the same netuids to perform arithmetic operations. + + A warning is raised if the netuids differ. + + Example: + >>> balance1 = Balance.from_rao(1000).set_unit(12) + >>> balance2 = Balance.from_rao(500).set_unit(12) + >>> balance1 + balance2 # No warning + + >>> balance3 = Balance.from_rao(200).set_unit(15) + >>> balance1 + balance3 # Raises DeprecationWarning + + In this example: + - `from_rao` creates a Balance instance from the amount in rao (smallest unit). + - `set_unit(12)` sets the unit to correspond to subnet 12 (i.e., Alpha from netuid 12). + """ + if self.netuid != other.netuid: + warnings.simplefilter("default", DeprecationWarning) + warnings.warn( + "Balance objects must have the same netuid (Alpha currency) to perform arithmetic operations.\n" + f"First balance is `{self}`. Second balance is `{other}`.\n\n" + "To create a Balance instance with the correct netuid, use:\n" + "Balance.from_rao(1000).set_unit(12) # 1000 rao in subnet 12", + category=DeprecationWarning, + stacklevel=2, + ) + + +class Balance: + """ + Represents the bittensor balance of the wallet, stored as rao (int). + This class provides a way to interact with balances in two different units: rao and tao. + It provides methods to convert between these units, as well as to perform arithmetic and comparison operations. + + Attributes: + unit (str): A string representing the symbol for the tao unit. + rao_unit (str): A string representing the symbol for the rao unit. + rao (int): An integer that stores the balance in rao units. + tao (float): A float property that gives the balance in tao units. + """ + + unit: str = settings.TAO_SYMBOL # This is the tao unit + rao_unit: str = settings.RAO_SYMBOL # This is the rao unit + rao: int + tao: float + netuid: int = 0 + + def __init__(self, balance: Union[int, float]): + """ + Initialize a Balance object. If balance is an int, it's assumed to be in rao. + If balance is a float, it's assumed to be in tao. + + Args: + balance: The initial balance, in either rao (if an int) or tao (if a float). + """ + if isinstance(balance, int): + self.rao = balance + elif isinstance(balance, float): + # Assume tao value for the float + self.rao = int(balance * pow(10, 9)) + else: + raise TypeError( + f"Balance must be an int (rao) or a float (tao), not `{type(balance)}`." + ) + + @property + def tao(self): + return self.rao / pow(10, 9) + + def __int__(self): + """Convert the Balance object to an int. The resulting value is in rao.""" + return self.rao + + def __float__(self): + """Convert the Balance object to a float. The resulting value is in tao.""" + return self.tao + + def __str__(self): + """ + Returns the Balance object as a string in the format "symbolvalue", where the value is in tao. + """ + if self.unit == UNITS[0]: + return f"{self.unit}{float(self.tao):,.9f}" + else: + return f"\u200e{float(self.tao):,.9f}{self.unit}\u200e" + + def __rich__(self): + int_tao, fract_tao = format(float(self.tao), "f").split(".") + return f"[green]{self.unit}{int_tao}.{fract_tao}[/green]" + + def __str_rao__(self): + return f"{self.rao_unit}{int(self.rao)}" + + def __rich_rao__(self): + return f"[green]{self.rao_unit}{int(self.rao)}[/green]" + + def __repr__(self): + return self.__str__() + + def __eq__(self, other: Union[int, float, "Balance"]): + if other is None: + return False + + if isinstance(other, Balance): + _check_currencies(self, other) + return self.rao == other.rao + else: + try: + # Attempt to cast to int from rao + other_rao = int(other) + return self.rao == other_rao + except (TypeError, ValueError): + raise NotImplementedError("Unsupported type") + + def __ne__(self, other: Union[int, float, "Balance"]): + return not self == other + + def __gt__(self, other: Union[int, float, "Balance"]): + if isinstance(other, Balance): + _check_currencies(self, other) + return self.rao > other.rao + else: + try: + # Attempt to cast to int from rao + other_rao = int(other) + return self.rao > other_rao + except ValueError: + raise NotImplementedError("Unsupported type") + + def __lt__(self, other: Union[int, float, "Balance"]): + if isinstance(other, Balance): + _check_currencies(self, other) + return self.rao < other.rao + else: + try: + # Attempt to cast to int from rao + other_rao = int(other) + return self.rao < other_rao + except ValueError: + raise NotImplementedError("Unsupported type") + + def __le__(self, other: Union[int, float, "Balance"]): + try: + if isinstance(other, Balance): + _check_currencies(self, other) + return self < other or self == other + except TypeError: + raise NotImplementedError("Unsupported type") + + def __ge__(self, other: Union[int, float, "Balance"]): + try: + if isinstance(other, Balance): + _check_currencies(self, other) + return self > other or self == other + except TypeError: + raise NotImplementedError("Unsupported type") + + def __add__(self, other: Union[int, float, "Balance"]): + if isinstance(other, Balance): + _check_currencies(self, other) + return Balance.from_rao(int(self.rao + other.rao)).set_unit(self.netuid) + else: + try: + # Attempt to cast to int from rao + return Balance.from_rao(int(self.rao + other)).set_unit(self.netuid) + except (ValueError, TypeError): + raise NotImplementedError("Unsupported type") + + def __radd__(self, other: Union[int, float, "Balance"]): + try: + if isinstance(other, Balance): + _check_currencies(self, other) + return self + other + except TypeError: + raise NotImplementedError("Unsupported type") + + def __sub__(self, other: Union[int, float, "Balance"]): + try: + if isinstance(other, Balance): + _check_currencies(self, other) + return self + -other + except TypeError: + raise NotImplementedError("Unsupported type") + + def __rsub__(self, other: Union[int, float, "Balance"]): + try: + if isinstance(other, Balance): + _check_currencies(self, other) + return -self + other + except TypeError: + raise NotImplementedError("Unsupported type") + + def __mul__(self, other: Union[int, float, "Balance"]): + if isinstance(other, Balance): + _check_currencies(self, other) + return Balance.from_rao(int(self.rao * other.rao)).set_unit(self.netuid) + else: + try: + # Attempt to cast to int from rao + return Balance.from_rao(int(self.rao * other)).set_unit(self.netuid) + except (ValueError, TypeError): + raise NotImplementedError("Unsupported type") + + def __rmul__(self, other: Union[int, float, "Balance"]): + if isinstance(other, Balance): + _check_currencies(self, other) + return self * other + + def __truediv__(self, other: Union[int, float, "Balance"]): + if isinstance(other, Balance): + _check_currencies(self, other) + return Balance.from_rao(int(self.rao / other.rao)).set_unit(self.netuid) + else: + try: + # Attempt to cast to int from rao + return Balance.from_rao(int(self.rao / other)).set_unit(self.netuid) + except (ValueError, TypeError): + raise NotImplementedError("Unsupported type") + + def __rtruediv__(self, other: Union[int, float, "Balance"]): + if isinstance(other, Balance): + _check_currencies(self, other) + return Balance.from_rao(int(other.rao / self.rao)).set_unit(self.netuid) + else: + try: + # Attempt to cast to int from rao + return Balance.from_rao(int(other / self.rao)).set_unit(self.netuid) + except (ValueError, TypeError): + raise NotImplementedError("Unsupported type") + + def __floordiv__(self, other: Union[int, float, "Balance"]): + if isinstance(other, Balance): + _check_currencies(self, other) + return Balance.from_rao(int(self.tao // other.tao)).set_unit(self.netuid) + else: + try: + # Attempt to cast to int from rao + return Balance.from_rao(int(self.rao // other)).set_unit(self.netuid) + except (ValueError, TypeError): + raise NotImplementedError("Unsupported type") + + def __rfloordiv__(self, other: Union[int, float, "Balance"]): + if isinstance(other, Balance): + _check_currencies(self, other) + return Balance.from_rao(int(other.rao // self.rao)).set_unit(self.netuid) + else: + try: + # Attempt to cast to int from rao + return Balance.from_rao(int(other // self.rao)).set_unit(self.netuid) + except (ValueError, TypeError): + raise NotImplementedError("Unsupported type") + + def __nonzero__(self) -> bool: + return bool(self.rao) + + def __neg__(self): + return Balance.from_rao(-self.rao).set_unit(self.netuid) + + def __pos__(self): + return Balance.from_rao(self.rao).set_unit(self.netuid) + + def __abs__(self): + return Balance.from_rao(abs(self.rao)).set_unit(self.netuid) + + @staticmethod + def from_float(amount: float, netuid: int = 0) -> "Balance": + """ + Given tao, return :func:`Balance` object with rao(``int``) and tao(``float``), where rao = int(tao*pow(10,9)) + Args: + amount (float): The amount in tao. + netuid (int): The subnet uid for set currency unit. Defaults to `0`. + + Returns: + A Balance object representing the given amount. + """ + rao_ = int(amount * pow(10, 9)) + return Balance(rao_).set_unit(netuid) + + @staticmethod + def from_tao(amount: float, netuid: int = 0) -> "Balance": + """ + Given tao, return Balance object with rao(``int``) and tao(``float``), where rao = int(tao*pow(10,9)) + + Args: + amount (float): The amount in tao. + netuid (int): The subnet uid for set currency unit. Defaults to `0`. + + Returns: + A Balance object representing the given amount. + """ + rao_ = int(amount * pow(10, 9)) + return Balance(rao_).set_unit(netuid) + + @staticmethod + def from_rao(amount: int, netuid: int = 0) -> "Balance": + """ + Given rao, return Balance object with rao(``int``) and tao(``float``), where rao = int(tao*pow(10,9)) + + Args: + amount (int): The amount in rao. + netuid (int): The subnet uid for set currency unit. Defaults to `0`. + + Returns: + A Balance object representing the given amount. + """ + return Balance(amount).set_unit(netuid) + + @staticmethod + def get_unit(netuid: int) -> str: + base = len(UNITS) + if netuid < base: + return UNITS[netuid] + else: + result = "" + while netuid > 0: + result = UNITS[netuid % base] + result + netuid //= base + return result + + def set_unit(self, netuid: int): + self.netuid = netuid + self.unit = Balance.get_unit(netuid) + self.rao_unit = Balance.get_unit(netuid) + return self + + +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: Union[FixedPoint, ScaleType], frac_bits: int = 64, total_bits: int = 128 +) -> float: + # 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) 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( + 0x03C4 + ), # τ Note: the subnet symbol for sn 0 is b"\xce\xa4" / Τ / Tau — however the currency/balance is τ (Tao) + b"\xce\xb1".decode(), # α (Alpha, 1) + b"\xce\xb2".decode(), # β (Beta, 2) + b"\xce\xb3".decode(), # γ (Gamma, 3) + b"\xce\xb4".decode(), # δ (Delta, 4) + b"\xce\xb5".decode(), # ε (Epsilon, 5) + b"\xce\xb6".decode(), # ζ (Zeta, 6) + b"\xce\xb7".decode(), # η (Eta, 7) + b"\xce\xb8".decode(), # θ (Theta, 8) + b"\xce\xb9".decode(), # ι (Iota, 9) + b"\xce\xba".decode(), # κ (Kappa, 10) + b"\xce\xbb".decode(), # λ (Lambda, 11) + b"\xce\xbc".decode(), # μ (Mu, 12) + b"\xce\xbd".decode(), # ν (Nu, 13) + b"\xce\xbe".decode(), # ξ (Xi, 14) + b"\xce\xbf".decode(), # ο (Omicron, 15) + b"\xcf\x80".decode(), # π (Pi, 16) + b"\xcf\x81".decode(), # ρ (Rho, 17) + b"\xcf\x83".decode(), # σ (Sigma, 18) + "t", # t (Tau, 19) + b"\xcf\x85".decode(), # υ (Upsilon, 20) + b"\xcf\x86".decode(), # φ (Phi, 21) + b"\xcf\x87".decode(), # χ (Chi, 22) + b"\xcf\x88".decode(), # ψ (Psi, 23) + b"\xcf\x89".decode(), # ω (Omega, 24) + b"\xd7\x90".decode(), # א (Aleph, 25) + b"\xd7\x91".decode(), # ב (Bet, 26) + b"\xd7\x92".decode(), # ג (Gimel, 27) + b"\xd7\x93".decode(), # ד (Dalet, 28) + b"\xd7\x94".decode(), # ה (He, 29) + b"\xd7\x95".decode(), # ו (Vav, 30) + b"\xd7\x96".decode(), # ז (Zayin, 31) + b"\xd7\x97".decode(), # ח (Het, 32) + b"\xd7\x98".decode(), # ט (Tet, 33) + b"\xd7\x99".decode(), # י (Yod, 34) + b"\xd7\x9a".decode(), # ך (Final Kaf, 35) + b"\xd7\x9b".decode(), # כ (Kaf, 36) + b"\xd7\x9c".decode(), # ל (Lamed, 37) + b"\xd7\x9d".decode(), # ם (Final Mem, 38) + b"\xd7\x9e".decode(), # מ (Mem, 39) + b"\xd7\x9f".decode(), # ן (Final Nun, 40) + b"\xd7\xa0".decode(), # נ (Nun, 41) + b"\xd7\xa1".decode(), # ס (Samekh, 42) + b"\xd7\xa2".decode(), # ע (Ayin, 43) + b"\xd7\xa3".decode(), # ף (Final Pe, 44) + b"\xd7\xa4".decode(), # פ (Pe, 45) + b"\xd7\xa5".decode(), # ץ (Final Tsadi, 46) + b"\xd7\xa6".decode(), # צ (Tsadi, 47) + b"\xd7\xa7".decode(), # ק (Qof, 48) + b"\xd7\xa8".decode(), # ר (Resh, 49) + b"\xd7\xa9".decode(), # ש (Shin, 50) + b"\xd7\xaa".decode(), # ת (Tav, 51) + b"\xd8\xa7".decode(), # ا (Alif, 52) + b"\xd8\xa8".decode(), # ب (Ba, 53) + b"\xd8\xaa".decode(), # ت (Ta, 54) + b"\xd8\xab".decode(), # ث (Tha, 55) + b"\xd8\xac".decode(), # ج (Jim, 56) + b"\xd8\xad".decode(), # ح (Ha, 57) + b"\xd8\xae".decode(), # خ (Kha, 58) + b"\xd8\xaf".decode(), # د (Dal, 59) + b"\xd8\xb0".decode(), # ذ (Dhal, 60) + b"\xd8\xb1".decode(), # ر (Ra, 61) + b"\xd8\xb2".decode(), # ز (Zay, 62) + b"\xd8\xb3".decode(), # س (Sin, 63) + b"\xd8\xb4".decode(), # ش (Shin, 64) + b"\xd8\xb5".decode(), # ص (Sad, 65) + b"\xd8\xb6".decode(), # ض (Dad, 66) + b"\xd8\xb7".decode(), # ط (Ta, 67) + b"\xd8\xb8".decode(), # ظ (Dha, 68) + b"\xd8\xb9".decode(), # ع (Ain, 69) + b"\xd8\xba".decode(), # غ (Ghayn, 70) + b"\xd9\x81".decode(), # ف (Fa, 71) + b"\xd9\x82".decode(), # ق (Qaf, 72) + b"\xd9\x83".decode(), # ك (Kaf, 73) + b"\xd9\x84".decode(), # ل (Lam, 74) + b"\xd9\x85".decode(), # م (Mim, 75) + b"\xd9\x86".decode(), # ن (Nun, 76) + b"\xd9\x87".decode(), # ه (Ha, 77) + b"\xd9\x88".decode(), # و (Waw, 78) + b"\xd9\x8a".decode(), # ي (Ya, 79) + b"\xd9\x89".decode(), # ى (Alef Maksura, 80) + b"\xe1\x9a\xa0".decode(), # ᚠ (Fehu, wealth, 81) + b"\xe1\x9a\xa2".decode(), # ᚢ (Uruz, strength, 82) + b"\xe1\x9a\xa6".decode(), # ᚦ (Thurisaz, giant, 83) + b"\xe1\x9a\xa8".decode(), # ᚨ (Ansuz, god, 84) + b"\xe1\x9a\xb1".decode(), # ᚱ (Raidho, ride, 85) + b"\xe1\x9a\xb3".decode(), # ᚲ (Kaunan, ulcer, 86) + b"\xd0\xab".decode(), # Ы (Cyrillic Yeru, 87) + b"\xe1\x9b\x89".decode(), # ᛉ (Algiz, protection, 88) + b"\xe1\x9b\x92".decode(), # ᛒ (Berkanan, birch, 89) + b"\xe1\x9a\x80".decode(), #   (Space, 90) + b"\xe1\x9a\x81".decode(), # ᚁ (Beith, birch, 91) + b"\xe1\x9a\x82".decode(), # ᚂ (Luis, rowan, 92) + b"\xe1\x9a\x83".decode(), # ᚃ (Fearn, alder, 93) + b"\xe1\x9a\x84".decode(), # ᚄ (Sail, willow, 94) + b"\xe1\x9a\x85".decode(), # ᚅ (Nion, ash, 95) + b"\xe1\x9a\x9b".decode(), # ᚛ (Forfeda, 96) + b"\xe1\x83\x90".decode(), # ა (Ani, 97) + b"\xe1\x83\x91".decode(), # ბ (Bani, 98) + b"\xe1\x83\x92".decode(), # გ (Gani, 99) + b"\xe1\x83\x93".decode(), # დ (Doni, 100) + b"\xe1\x83\x94".decode(), # ე (Eni, 101) + b"\xe1\x83\x95".decode(), # ვ (Vini, 102) + b"\xd4\xb1".decode(), # Ա (Ayp, 103) + b"\xd4\xb2".decode(), # Բ (Ben, 104) + b"\xd4\xb3".decode(), # Գ (Gim, 105) + b"\xd4\xb4".decode(), # Դ (Da, 106) + b"\xd4\xb5".decode(), # Ե (Ech, 107) + b"\xd4\xb6".decode(), # Զ (Za, 108) + b"\xd5\x9e".decode(), # ՞ (Question mark, 109) + b"\xd0\x80".decode(), # Ѐ (Ie with grave, 110) + b"\xd0\x81".decode(), # Ё (Io, 111) + b"\xd0\x82".decode(), # Ђ (Dje, 112) + b"\xd0\x83".decode(), # Ѓ (Gje, 113) + b"\xd0\x84".decode(), # Є (Ukrainian Ie, 114) + b"\xd0\x85".decode(), # Ѕ (Dze, 115) + b"\xd1\x8a".decode(), # Ъ (Hard sign, 116) + b"\xe2\xb2\x80".decode(), # Ⲁ (Alfa, 117) + b"\xe2\xb2\x81".decode(), # ⲁ (Small Alfa, 118) + b"\xe2\xb2\x82".decode(), # Ⲃ (Vida, 119) + b"\xe2\xb2\x83".decode(), # ⲃ (Small Vida, 120) + b"\xe2\xb2\x84".decode(), # Ⲅ (Gamma, 121) + b"\xe2\xb2\x85".decode(), # ⲅ (Small Gamma, 122) + b"\xf0\x91\x80\x80".decode(), # 𑀀 (A, 123) + b"\xf0\x91\x80\x81".decode(), # 𑀁 (Aa, 124) + b"\xf0\x91\x80\x82".decode(), # 𑀂 (I, 125) + b"\xf0\x91\x80\x83".decode(), # 𑀃 (Ii, 126) + b"\xf0\x91\x80\x85".decode(), # 𑀅 (U, 127) + b"\xe0\xb6\xb1".decode(), # ඲ (La, 128) + b"\xe0\xb6\xb2".decode(), # ඳ (Va, 129) + b"\xe0\xb6\xb3".decode(), # ප (Sha, 130) + b"\xe0\xb6\xb4".decode(), # ඵ (Ssa, 131) + b"\xe0\xb6\xb5".decode(), # බ (Sa, 132) + b"\xe0\xb6\xb6".decode(), # භ (Ha, 133) + b"\xe2\xb0\x80".decode(), # Ⰰ (Az, 134) + b"\xe2\xb0\x81".decode(), # Ⰱ (Buky, 135) + b"\xe2\xb0\x82".decode(), # Ⰲ (Vede, 136) + b"\xe2\xb0\x83".decode(), # Ⰳ (Glagoli, 137) + b"\xe2\xb0\x84".decode(), # Ⰴ (Dobro, 138) + b"\xe2\xb0\x85".decode(), # Ⰵ (Yest, 139) + b"\xe2\xb0\x86".decode(), # Ⰶ (Zhivete, 140) + b"\xe2\xb0\x87".decode(), # Ⰷ (Zemlja, 141) + b"\xe2\xb0\x88".decode(), # Ⰸ (Izhe, 142) + b"\xe2\xb0\x89".decode(), # Ⰹ (Initial Izhe, 143) + b"\xe2\xb0\x8a".decode(), # Ⰺ (I, 144) + b"\xe2\xb0\x8b".decode(), # Ⰻ (Djerv, 145) + b"\xe2\xb0\x8c".decode(), # Ⰼ (Kako, 146) + b"\xe2\xb0\x8d".decode(), # Ⰽ (Ljudije, 147) + b"\xe2\xb0\x8e".decode(), # Ⰾ (Myse, 148) + b"\xe2\xb0\x8f".decode(), # Ⰿ (Nash, 149) + b"\xe2\xb0\x90".decode(), # Ⱀ (On, 150) + b"\xe2\xb0\x91".decode(), # Ⱁ (Pokoj, 151) + b"\xe2\xb0\x92".decode(), # Ⱂ (Rtsy, 152) + b"\xe2\xb0\x93".decode(), # Ⱃ (Slovo, 153) + b"\xe2\xb0\x94".decode(), # Ⱄ (Tvrido, 154) + b"\xe2\xb0\x95".decode(), # Ⱅ (Uku, 155) + b"\xe2\xb0\x96".decode(), # Ⱆ (Fert, 156) + b"\xe2\xb0\x97".decode(), # Ⱇ (Xrivi, 157) + b"\xe2\xb0\x98".decode(), # Ⱈ (Ot, 158) + b"\xe2\xb0\x99".decode(), # Ⱉ (Cy, 159) + b"\xe2\xb0\x9a".decode(), # Ⱊ (Shcha, 160) + b"\xe2\xb0\x9b".decode(), # Ⱋ (Er, 161) + b"\xe2\xb0\x9c".decode(), # Ⱌ (Yeru, 162) + b"\xe2\xb0\x9d".decode(), # Ⱍ (Small Yer, 163) + b"\xe2\xb0\x9e".decode(), # Ⱎ (Yo, 164) + b"\xe2\xb0\x9f".decode(), # Ⱏ (Yu, 165) + b"\xe2\xb0\xa0".decode(), # Ⱐ (Ja, 166) + b"\xe0\xb8\x81".decode(), # ก (Ko Kai, 167) + b"\xe0\xb8\x82".decode(), # ข (Kho Khai, 168) + b"\xe0\xb8\x83".decode(), # ฃ (Kho Khuat, 169) + b"\xe0\xb8\x84".decode(), # ค (Kho Khon, 170) + b"\xe0\xb8\x85".decode(), # ฅ (Kho Rakhang, 171) + b"\xe0\xb8\x86".decode(), # ฆ (Kho Khwai, 172) + b"\xe0\xb8\x87".decode(), # ง (Ngo Ngu, 173) + b"\xe0\xb8\x88".decode(), # จ (Cho Chan, 174) + b"\xe0\xb8\x89".decode(), # ฉ (Cho Ching, 175) + b"\xe0\xb8\x8a".decode(), # ช (Cho Chang, 176) + b"\xe0\xb8\x8b".decode(), # ซ (So So, 177) + b"\xe0\xb8\x8c".decode(), # ฌ (Cho Choe, 178) + b"\xe0\xb8\x8d".decode(), # ญ (Yo Ying, 179) + b"\xe0\xb8\x8e".decode(), # ฎ (Do Chada, 180) + b"\xe0\xb8\x8f".decode(), # ฏ (To Patak, 181) + b"\xe0\xb8\x90".decode(), # ฐ (Tho Than, 182) + b"\xe0\xb8\x91".decode(), # ฑ (Tho Nangmontho, 183) + b"\xe0\xb8\x92".decode(), # ฒ (Tho Phuthao, 184) + b"\xe0\xb8\x93".decode(), # ณ (No Nen, 185) + b"\xe0\xb8\x94".decode(), # ด (Do Dek, 186) + b"\xe0\xb8\x95".decode(), # ต (To Tao, 187) + b"\xe0\xb8\x96".decode(), # ถ (Tho Thung, 188) + b"\xe0\xb8\x97".decode(), # ท (Tho Thahan, 189) + b"\xe0\xb8\x98".decode(), # ธ (Tho Thong, 190) + b"\xe0\xb8\x99".decode(), # น (No Nu, 191) + b"\xe0\xb8\x9a".decode(), # บ (Bo Baimai, 192) + b"\xe0\xb8\x9b".decode(), # ป (Po Pla, 193) + b"\xe0\xb8\x9c".decode(), # ผ (Pho Phung, 194) + b"\xe0\xb8\x9d".decode(), # ฝ (Fo Fa, 195) + b"\xe0\xb8\x9e".decode(), # พ (Pho Phan, 196) + b"\xe0\xb8\x9f".decode(), # ฟ (Fo Fan, 197) + b"\xe0\xb8\xa0".decode(), # ภ (Pho Samphao, 198) + b"\xe0\xb8\xa1".decode(), # ม (Mo Ma, 199) + b"\xe0\xb8\xa2".decode(), # ย (Yo Yak, 200) + b"\xe0\xb8\xa3".decode(), # ร (Ro Rua, 201) + b"\xe0\xb8\xa5".decode(), # ล (Lo Ling, 202) + b"\xe0\xb8\xa7".decode(), # ว (Wo Waen, 203) + b"\xe0\xb8\xa8".decode(), # ศ (So Sala, 204) + b"\xe0\xb8\xa9".decode(), # ษ (So Rusi, 205) + b"\xe0\xb8\xaa".decode(), # ส (So Sua, 206) + b"\xe0\xb8\xab".decode(), # ห (Ho Hip, 207) + b"\xe0\xb8\xac".decode(), # ฬ (Lo Chula, 208) + b"\xe0\xb8\xad".decode(), # อ (O Ang, 209) + b"\xe0\xb8\xae".decode(), # ฮ (Ho Nokhuk, 210) + b"\xe1\x84\x80".decode(), # ㄱ (Giyeok, 211) + b"\xe1\x84\x81".decode(), # ㄴ (Nieun, 212) + b"\xe1\x84\x82".decode(), # ㄷ (Digeut, 213) + b"\xe1\x84\x83".decode(), # ㄹ (Rieul, 214) + b"\xe1\x84\x84".decode(), # ㅁ (Mieum, 215) + b"\xe1\x84\x85".decode(), # ㅂ (Bieup, 216) + b"\xe1\x84\x86".decode(), # ㅅ (Siot, 217) + b"\xe1\x84\x87".decode(), # ㅇ (Ieung, 218) + b"\xe1\x84\x88".decode(), # ㅈ (Jieut, 219) + b"\xe1\x84\x89".decode(), # ㅊ (Chieut, 220) + b"\xe1\x84\x8a".decode(), # ㅋ (Kieuk, 221) + b"\xe1\x84\x8b".decode(), # ㅌ (Tieut, 222) + b"\xe1\x84\x8c".decode(), # ㅍ (Pieup, 223) + b"\xe1\x84\x8d".decode(), # ㅎ (Hieut, 224) + b"\xe1\x85\xa1".decode(), # ㅏ (A, 225) + b"\xe1\x85\xa2".decode(), # ㅐ (Ae, 226) + b"\xe1\x85\xa3".decode(), # ㅑ (Ya, 227) + b"\xe1\x85\xa4".decode(), # ㅒ (Yae, 228) + b"\xe1\x85\xa5".decode(), # ㅓ (Eo, 229) + b"\xe1\x85\xa6".decode(), # ㅔ (E, 230) + b"\xe1\x85\xa7".decode(), # ㅕ (Yeo, 231) + b"\xe1\x85\xa8".decode(), # ㅖ (Ye, 232) + b"\xe1\x85\xa9".decode(), # ㅗ (O, 233) + b"\xe1\x85\xaa".decode(), # ㅘ (Wa, 234) + b"\xe1\x85\xab".decode(), # ㅙ (Wae, 235) + b"\xe1\x85\xac".decode(), # ㅚ (Oe, 236) + b"\xe1\x85\xad".decode(), # ㅛ (Yo, 237) + b"\xe1\x85\xae".decode(), # ㅜ (U, 238) + b"\xe1\x85\xaf".decode(), # ㅝ (Weo, 239) + b"\xe1\x85\xb0".decode(), # ㅞ (We, 240) + b"\xe1\x85\xb1".decode(), # ㅟ (Wi, 241) + b"\xe1\x85\xb2".decode(), # ㅠ (Yu, 242) + b"\xe1\x85\xb3".decode(), # ㅡ (Eu, 243) + b"\xe1\x85\xb4".decode(), # ㅢ (Ui, 244) + b"\xe1\x85\xb5".decode(), # ㅣ (I, 245) + b"\xe1\x8a\xa0".decode(), # አ (Glottal A, 246) + b"\xe1\x8a\xa1".decode(), # ኡ (Glottal U, 247) + b"\xe1\x8a\xa2".decode(), # ኢ (Glottal I, 248) + b"\xe1\x8a\xa3".decode(), # ኣ (Glottal Aa, 249) + b"\xe1\x8a\xa4".decode(), # ኤ (Glottal E, 250) + b"\xe1\x8a\xa5".decode(), # እ (Glottal Ie, 251) + b"\xe1\x8a\xa6".decode(), # ኦ (Glottal O, 252) + b"\xe1\x8a\xa7".decode(), # ኧ (Glottal Wa, 253) + b"\xe1\x8b\x88".decode(), # ወ (Wa, 254) + b"\xe1\x8b\x89".decode(), # ዉ (Wu, 255) + b"\xe1\x8b\x8a".decode(), # ዊ (Wi, 256) + b"\xe1\x8b\x8b".decode(), # ዋ (Waa, 257) + b"\xe1\x8b\x8c".decode(), # ዌ (We, 258) + b"\xe1\x8b\x8d".decode(), # ው (Wye, 259) + b"\xe1\x8b\x8e".decode(), # ዎ (Wo, 260) + b"\xe1\x8a\xb0".decode(), # ኰ (Ko, 261) + b"\xe1\x8a\xb1".decode(), # ኱ (Ku, 262) + b"\xe1\x8a\xb2".decode(), # ኲ (Ki, 263) + b"\xe1\x8a\xb3".decode(), # ኳ (Kua, 264) + b"\xe1\x8a\xb4".decode(), # ኴ (Ke, 265) + b"\xe1\x8a\xb5".decode(), # ኵ (Kwe, 266) + b"\xe1\x8a\xb6".decode(), # ኶ (Ko, 267) + b"\xe1\x8a\x90".decode(), # ጐ (Go, 268) + b"\xe1\x8a\x91".decode(), # ጑ (Gu, 269) + b"\xe1\x8a\x92".decode(), # ጒ (Gi, 270) + b"\xe1\x8a\x93".decode(), # መ (Gua, 271) + b"\xe1\x8a\x94".decode(), # ጔ (Ge, 272) + b"\xe1\x8a\x95".decode(), # ጕ (Gwe, 273) + b"\xe1\x8a\x96".decode(), # ጖ (Go, 274) + b"\xe0\xa4\x85".decode(), # अ (A, 275) + b"\xe0\xa4\x86".decode(), # आ (Aa, 276) + b"\xe0\xa4\x87".decode(), # इ (I, 277) + b"\xe0\xa4\x88".decode(), # ई (Ii, 278) + b"\xe0\xa4\x89".decode(), # उ (U, 279) + b"\xe0\xa4\x8a".decode(), # ऊ (Uu, 280) + b"\xe0\xa4\x8b".decode(), # ऋ (R, 281) + b"\xe0\xa4\x8f".decode(), # ए (E, 282) + b"\xe0\xa4\x90".decode(), # ऐ (Ai, 283) + b"\xe0\xa4\x93".decode(), # ओ (O, 284) + b"\xe0\xa4\x94".decode(), # औ (Au, 285) + b"\xe0\xa4\x95".decode(), # क (Ka, 286) + b"\xe0\xa4\x96".decode(), # ख (Kha, 287) + b"\xe0\xa4\x97".decode(), # ग (Ga, 288) + b"\xe0\xa4\x98".decode(), # घ (Gha, 289) + b"\xe0\xa4\x99".decode(), # ङ (Nga, 290) + b"\xe0\xa4\x9a".decode(), # च (Cha, 291) + b"\xe0\xa4\x9b".decode(), # छ (Chha, 292) + b"\xe0\xa4\x9c".decode(), # ज (Ja, 293) + b"\xe0\xa4\x9d".decode(), # झ (Jha, 294) + b"\xe0\xa4\x9e".decode(), # ञ (Nya, 295) + b"\xe0\xa4\x9f".decode(), # ट (Ta, 296) + b"\xe0\xa4\xa0".decode(), # ठ (Tha, 297) + b"\xe0\xa4\xa1".decode(), # ड (Da, 298) + b"\xe0\xa4\xa2".decode(), # ढ (Dha, 299) + b"\xe0\xa4\xa3".decode(), # ण (Na, 300) + b"\xe0\xa4\xa4".decode(), # त (Ta, 301) + b"\xe0\xa4\xa5".decode(), # थ (Tha, 302) + b"\xe0\xa4\xa6".decode(), # द (Da, 303) + b"\xe0\xa4\xa7".decode(), # ध (Dha, 304) + b"\xe0\xa4\xa8".decode(), # न (Na, 305) + b"\xe0\xa4\xaa".decode(), # प (Pa, 306) + b"\xe0\xa4\xab".decode(), # फ (Pha, 307) + b"\xe0\xa4\xac".decode(), # ब (Ba, 308) + b"\xe0\xa4\xad".decode(), # भ (Bha, 309) + b"\xe0\xa4\xae".decode(), # म (Ma, 310) + b"\xe0\xa4\xaf".decode(), # य (Ya, 311) + b"\xe0\xa4\xb0".decode(), # र (Ra, 312) + b"\xe0\xa4\xb2".decode(), # ल (La, 313) + b"\xe0\xa4\xb5".decode(), # व (Va, 314) + b"\xe0\xa4\xb6".decode(), # श (Sha, 315) + b"\xe0\xa4\xb7".decode(), # ष (Ssa, 316) + b"\xe0\xa4\xb8".decode(), # स (Sa, 317) + b"\xe0\xa4\xb9".decode(), # ह (Ha, 318) + b"\xe3\x82\xa2".decode(), # ア (A, 319) + b"\xe3\x82\xa4".decode(), # イ (I, 320) + b"\xe3\x82\xa6".decode(), # ウ (U, 321) + b"\xe3\x82\xa8".decode(), # エ (E, 322) + b"\xe3\x82\xaa".decode(), # オ (O, 323) + b"\xe3\x82\xab".decode(), # カ (Ka, 324) + b"\xe3\x82\xad".decode(), # キ (Ki, 325) + b"\xe3\x82\xaf".decode(), # ク (Ku, 326) + b"\xe3\x82\xb1".decode(), # ケ (Ke, 327) + b"\xe3\x82\xb3".decode(), # コ (Ko, 328) + b"\xe3\x82\xb5".decode(), # サ (Sa, 329) + b"\xe3\x82\xb7".decode(), # シ (Shi, 330) + b"\xe3\x82\xb9".decode(), # ス (Su, 331) + b"\xe3\x82\xbb".decode(), # セ (Se, 332) + b"\xe3\x82\xbd".decode(), # ソ (So, 333) + b"\xe3\x82\xbf".decode(), # タ (Ta, 334) + b"\xe3\x83\x81".decode(), # チ (Chi, 335) + b"\xe3\x83\x84".decode(), # ツ (Tsu, 336) + b"\xe3\x83\x86".decode(), # テ (Te, 337) + b"\xe3\x83\x88".decode(), # ト (To, 338) + b"\xe3\x83\x8a".decode(), # ナ (Na, 339) + b"\xe3\x83\x8b".decode(), # ニ (Ni, 340) + b"\xe3\x83\x8c".decode(), # ヌ (Nu, 341) + b"\xe3\x83\x8d".decode(), # ネ (Ne, 342) + b"\xe3\x83\x8e".decode(), # ノ (No, 343) + b"\xe3\x83\x8f".decode(), # ハ (Ha, 344) + b"\xe3\x83\x92".decode(), # ヒ (Hi, 345) + b"\xe3\x83\x95".decode(), # フ (Fu, 346) + b"\xe3\x83\x98".decode(), # ヘ (He, 347) + b"\xe3\x83\x9b".decode(), # ホ (Ho, 348) + b"\xe3\x83\x9e".decode(), # マ (Ma, 349) + b"\xe3\x83\x9f".decode(), # ミ (Mi, 350) + b"\xe3\x83\xa0".decode(), # ム (Mu, 351) + b"\xe3\x83\xa1".decode(), # メ (Me, 352) + b"\xe3\x83\xa2".decode(), # モ (Mo, 353) + b"\xe3\x83\xa4".decode(), # ヤ (Ya, 354) + b"\xe3\x83\xa6".decode(), # ユ (Yu, 355) + b"\xe3\x83\xa8".decode(), # ヨ (Yo, 356) + b"\xe3\x83\xa9".decode(), # ラ (Ra, 357) + b"\xe3\x83\xaa".decode(), # リ (Ri, 358) + b"\xe3\x83\xab".decode(), # ル (Ru, 359) + b"\xe3\x83\xac".decode(), # レ (Re, 360) + b"\xe3\x83\xad".decode(), # ロ (Ro, 361) + b"\xe3\x83\xaf".decode(), # ワ (Wa, 362) + b"\xe3\x83\xb2".decode(), # ヲ (Wo, 363) + b"\xe3\x83\xb3".decode(), # ン (N, 364) + b"\xe2\xb4\xb0".decode(), # ⴰ (Ya, 365) + b"\xe2\xb4\xb1".decode(), # ⴱ (Yab, 366) + b"\xe2\xb4\xb2".decode(), # ⴲ (Yabh, 367) + b"\xe2\xb4\xb3".decode(), # ⴳ (Yag, 368) + b"\xe2\xb4\xb4".decode(), # ⴴ (Yagh, 369) + b"\xe2\xb4\xb5".decode(), # ⴵ (Yaj, 370) + b"\xe2\xb4\xb6".decode(), # ⴶ (Yach, 371) + b"\xe2\xb4\xb7".decode(), # ⴷ (Yad, 372) + b"\xe2\xb4\xb8".decode(), # ⴸ (Yadh, 373) + b"\xe2\xb4\xb9".decode(), # ⴹ (Yadh, emphatic, 374) + b"\xe2\xb4\xba".decode(), # ⴺ (Yaz, 375) + b"\xe2\xb4\xbb".decode(), # ⴻ (Yazh, 376) + b"\xe2\xb4\xbc".decode(), # ⴼ (Yaf, 377) + b"\xe2\xb4\xbd".decode(), # ⴽ (Yak, 378) + b"\xe2\xb4\xbe".decode(), # ⴾ (Yak, variant, 379) + b"\xe2\xb4\xbf".decode(), # ⴿ (Yaq, 380) + b"\xe2\xb5\x80".decode(), # ⵀ (Yah, 381) + b"\xe2\xb5\x81".decode(), # ⵁ (Yahh, 382) + b"\xe2\xb5\x82".decode(), # ⵂ (Yahl, 383) + b"\xe2\xb5\x83".decode(), # ⵃ (Yahm, 384) + b"\xe2\xb5\x84".decode(), # ⵄ (Yayn, 385) + b"\xe2\xb5\x85".decode(), # ⵅ (Yakh, 386) + b"\xe2\xb5\x86".decode(), # ⵆ (Yakl, 387) + b"\xe2\xb5\x87".decode(), # ⵇ (Yahq, 388) + b"\xe2\xb5\x88".decode(), # ⵈ (Yash, 389) + b"\xe2\xb5\x89".decode(), # ⵉ (Yi, 390) + b"\xe2\xb5\x8a".decode(), # ⵊ (Yij, 391) + b"\xe2\xb5\x8b".decode(), # ⵋ (Yizh, 392) + b"\xe2\xb5\x8c".decode(), # ⵌ (Yink, 393) + b"\xe2\xb5\x8d".decode(), # ⵍ (Yal, 394) + b"\xe2\xb5\x8e".decode(), # ⵎ (Yam, 395) + b"\xe2\xb5\x8f".decode(), # ⵏ (Yan, 396) + b"\xe2\xb5\x90".decode(), # ⵐ (Yang, 397) + b"\xe2\xb5\x91".decode(), # ⵑ (Yany, 398) + b"\xe2\xb5\x92".decode(), # ⵒ (Yap, 399) + b"\xe2\xb5\x93".decode(), # ⵓ (Yu, 400) + b"\xe0\xb6\x85".decode(), # අ (A, 401) + b"\xe0\xb6\x86".decode(), # ආ (Aa, 402) + b"\xe0\xb6\x87".decode(), # ඉ (I, 403) + b"\xe0\xb6\x88".decode(), # ඊ (Ii, 404) + b"\xe0\xb6\x89".decode(), # උ (U, 405) + b"\xe0\xb6\x8a".decode(), # ඌ (Uu, 406) + b"\xe0\xb6\x8b".decode(), # ඍ (R, 407) + b"\xe0\xb6\x8c".decode(), # ඎ (Rr, 408) + b"\xe0\xb6\x8f".decode(), # ඏ (L, 409) + b"\xe0\xb6\x90".decode(), # ඐ (Ll, 410) + b"\xe0\xb6\x91".decode(), # එ (E, 411) + b"\xe0\xb6\x92".decode(), # ඒ (Ee, 412) + b"\xe0\xb6\x93".decode(), # ඓ (Ai, 413) + b"\xe0\xb6\x94".decode(), # ඔ (O, 414) + b"\xe0\xb6\x95".decode(), # ඕ (Oo, 415) + b"\xe0\xb6\x96".decode(), # ඖ (Au, 416) + b"\xe0\xb6\x9a".decode(), # ක (Ka, 417) + b"\xe0\xb6\x9b".decode(), # ඛ (Kha, 418) + b"\xe0\xb6\x9c".decode(), # ග (Ga, 419) + b"\xe0\xb6\x9d".decode(), # ඝ (Gha, 420) + b"\xe0\xb6\x9e".decode(), # ඞ (Nga, 421) + b"\xe0\xb6\x9f".decode(), # ච (Cha, 422) + b"\xe0\xb6\xa0".decode(), # ඡ (Chha, 423) + b"\xe0\xb6\xa1".decode(), # ජ (Ja, 424) + b"\xe0\xb6\xa2".decode(), # ඣ (Jha, 425) + b"\xe0\xb6\xa3".decode(), # ඤ (Nya, 426) + b"\xe0\xb6\xa4".decode(), # ට (Ta, 427) + b"\xe0\xb6\xa5".decode(), # ඥ (Tha, 428) + b"\xe0\xb6\xa6".decode(), # ඦ (Da, 429) + b"\xe0\xb6\xa7".decode(), # ට (Dha, 430) + b"\xe0\xb6\xa8".decode(), # ඨ (Na, 431) + b"\xe0\xb6\xaa".decode(), # ඪ (Pa, 432) + b"\xe0\xb6\xab".decode(), # ණ (Pha, 433) + b"\xe0\xb6\xac".decode(), # ඬ (Ba, 434) + b"\xe0\xb6\xad".decode(), # ත (Bha, 435) + b"\xe0\xb6\xae".decode(), # ථ (Ma, 436) + b"\xe0\xb6\xaf".decode(), # ද (Ya, 437) + b"\xe0\xb6\xb0".decode(), # ධ (Ra, 438) +] + + +def tao(amount: float, netuid: int = 0) -> Balance: + """ + Helper function to create a Balance object from a float (Tao) + """ + return Balance.from_tao(amount).set_unit(netuid) + + +def rao(amount: int, netuid: int = 0) -> Balance: + """ + Helper function to create a Balance object from an int (Rao) + """ + return Balance.from_rao(amount).set_unit(netuid) + + +def check_and_convert_to_balance( + amount: Union[float, int, Optional[Balance]], +) -> Balance: + """ + Helper function to check and convert the amount type to a Balance object. + This is used to support backwards compatibility while also providing a deprecation notice. + """ + if isinstance(amount, (float, int)): + deprecated_message( + "Detected a non-balance amount. Converting to Balance from Tao for backwards compatibility." + "Please update your code to use tao(amount) or Balance.from_tao(amount) for the main release 10.0.0." + ) + amount = tao(amount) + return amount diff --git a/bittensor/utils/batch_transforms.py b/bittensor/utils/batch_transforms.py deleted file mode 100644 index da137bf787..0000000000 --- a/bittensor/utils/batch_transforms.py +++ /dev/null @@ -1,146 +0,0 @@ -import torch - - -class ToTensor: - """Applies the :class:`~torchvision.transforms.ToTensor` transform to a batch of images. - """ - - def __init__(self): - self.max = 255 - - def __call__(self, tensor): - """ - Args: - tensor (Tensor): Tensor of size (N, C, H, W) to be tensorized. - Returns: - Tensor: Tensorized Tensor. - """ - return tensor.float().div_(self.max) - - -class Normalize: - """Applies the :class:`~torchvision.transforms.Normalize` transform to a batch of images. - .. note:: - This transform acts out of place by default, i.e., it does not mutate the input tensor. - Args: - mean (sequence): Sequence of means for each channel. - std (sequence): Sequence of standard deviations for each channel. - inplace(bool,optional): Bool to make this operation in-place. - dtype (torch.dtype,optional): The data type of tensors to which the transform will be applied. - device (torch.device,optional): The device of tensors to which the transform will be applied. - """ - - def __init__(self, - mean, - std, - inplace=False, - dtype=torch.float, - device=None): - - if not device: - device = torch.device( - "cuda" if torch.cuda.is_available() else "cpu") - - self.mean = torch.as_tensor(mean, dtype=dtype, - device=device)[None, :, None, None] - self.std = torch.as_tensor(std, dtype=dtype, device=device)[None, :, - None, None] - self.inplace = inplace - - def __call__(self, tensor): - """ - Args: - tensor (Tensor): Tensor of size (N, C, H, W) to be normalized. - Returns: - Tensor: Normalized Tensor. - """ - if not self.inplace: - tensor = tensor.clone() - - tensor.sub_(self.mean).div_(self.std) - return tensor - - -class RandomHorizontalFlip: - """Applies the :class:`~torchvision.transforms.RandomHorizontalFlip` transform to a batch of images. - .. note:: - This transform acts out of place by default, i.e., it does not mutate the input tensor. - Args: - p (float): probability of an image being flipped. - inplace(bool,optional): Bool to make this operation in-place. - """ - - def __init__(self, p=0.5, inplace=False): - self.p = p - self.inplace = inplace - - def __call__(self, tensor): - """ - Args: - tensor (Tensor): Tensor of size (N, C, H, W) to be flipped. - Returns: - Tensor: Randomly flipped Tensor. - """ - if not self.inplace: - tensor = tensor.clone() - - flipped = torch.rand(tensor.size(0)) < self.p - tensor[flipped] = torch.flip(tensor[flipped], [3]) - return tensor - - -class RandomCrop: - """Applies the :class:`~torchvision.transforms.RandomCrop` transform to a batch of images. - Args: - size (int): Desired output size of the crop. - padding (int, optional): Optional padding on each border of the image. - Default is None, i.e no padding. - dtype (torch.dtype,optional): The data type of tensors to which the transform will be applied. - device (torch.device,optional): The device of tensors to which the transform will be applied. - """ - - def __init__(self, size, padding=None, dtype=torch.float, device='cpu'): - self.size = size - self.padding = padding - self.dtype = dtype - self.device = device - - def __call__(self, tensor): - """ - Args: - tensor (Tensor): Tensor of size (N, C, H, W) to be cropped. - Returns: - Tensor: Randomly cropped Tensor. - """ - if self.padding is not None: - padded = torch.zeros( - (tensor.size(0), tensor.size(1), tensor.size(2) + - self.padding * 2, tensor.size(3) + self.padding * 2), - dtype=self.dtype, - device=self.device) - padded[:, :, self.padding:-self.padding, - self.padding:-self.padding] = tensor - else: - padded = tensor - - w, h = padded.size(2), padded.size(3) - th, tw = self.size, self.size - if w == tw and h == th: - i, j = 0, 0 - else: - i = torch.randint(0, - h - th + 1, (tensor.size(0),), - device=self.device) - j = torch.randint(0, - w - tw + 1, (tensor.size(0),), - device=self.device) - - rows = torch.arange(th, dtype=torch.long, device=self.device) + i[:, - None] - columns = torch.arange(tw, dtype=torch.long, - device=self.device) + j[:, None] - padded = padded.permute(1, 0, 2, 3) - padded = padded[:, - torch.arange(tensor.size(0))[:, None, None], - rows[:, torch.arange(th)[:, None]], columns[:, None]] - return padded.permute(1, 0, 2, 3) diff --git a/bittensor/utils/btlogging/__init__.py b/bittensor/utils/btlogging/__init__.py new file mode 100644 index 0000000000..9fee61cd80 --- /dev/null +++ b/bittensor/utils/btlogging/__init__.py @@ -0,0 +1,11 @@ +""" +btlogging sub-package standardized logging for Bittensor. + +This module provides logging functionality for the Bittensor package. It includes custom loggers, handlers, and +formatters to ensure consistent logging throughout the project. +""" + +from .loggingmachine import LoggingMachine + + +logging = LoggingMachine(LoggingMachine.config()) diff --git a/bittensor/utils/btlogging/console.py b/bittensor/utils/btlogging/console.py new file mode 100644 index 0000000000..20e09bdf60 --- /dev/null +++ b/bittensor/utils/btlogging/console.py @@ -0,0 +1,73 @@ +""" +BittensorConsole class gives the ability to log messages to the terminal without changing Bittensor logging level. + +Example: + from bittensor import logging + + # will be logged + logging.console.info("info message") + logging.console.error("error message") + logging.console.success("success message") + logging.console.warning("warning message") + logging.console.critical("critical message") + + # will not be logged + logging.info("test info") +""" + +from functools import wraps +from typing import Callable, TYPE_CHECKING + +from .helpers import all_loggers + +if TYPE_CHECKING: + from .loggingmachine import LoggingMachine + + +def _print_wrapper(func: "Callable"): + @wraps(func) + def wrapper(self: "BittensorConsole", *args, **kwargs): + """A wrapper function to temporarily set the logger level to debug.""" + old_logger_level = self.logger.get_level() + self.logger.set_console() + func(self, *args, **kwargs) + + for logger in all_loggers(): + logger.setLevel(old_logger_level) + + return wrapper + + +class BittensorConsole: + def __init__(self, logger: "LoggingMachine"): + self.logger = logger + + @_print_wrapper + def debug(self, message: str): + """Logs a DEBUG message to the console.""" + self.logger.debug(message) + + @_print_wrapper + def info(self, message: str): + """Logs a INFO message to the console.""" + self.logger.info(message) + + @_print_wrapper + def success(self, message: str): + """Logs a SUCCESS message to the console.""" + self.logger.success(message) + + @_print_wrapper + def warning(self, message: str): + """Logs a WARNING message to the console.""" + self.logger.warning(message) + + @_print_wrapper + def error(self, message: str): + """Logs a ERROR message to the console.""" + self.logger.error(message) + + @_print_wrapper + def critical(self, message: str): + """Logs a CRITICAL message to the console.""" + self.logger.critical(message) diff --git a/bittensor/utils/btlogging/defines.py b/bittensor/utils/btlogging/defines.py new file mode 100644 index 0000000000..f71a99e702 --- /dev/null +++ b/bittensor/utils/btlogging/defines.py @@ -0,0 +1,11 @@ +"""Btlogging constant definition module.""" + +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" +) +DATE_FORMAT = "%Y-%m-%d %H:%M:%S" +BITTENSOR_LOGGER_NAME = "bittensor" +DEFAULT_LOG_FILE_NAME = "bittensor.log" +DEFAULT_MAX_ROTATING_LOG_FILE_SIZE = 25 * 1024 * 1024 +DEFAULT_LOG_BACKUP_COUNT = 10 diff --git a/bittensor/utils/btlogging/format.py b/bittensor/utils/btlogging/format.py new file mode 100644 index 0000000000..989a49b925 --- /dev/null +++ b/bittensor/utils/btlogging/format.py @@ -0,0 +1,214 @@ +""" +btlogging.format module + +This module defines custom logging formatters for the Bittensor project. +""" + +import logging +import time +from typing import Optional +from colorama import init, Fore, Back, Style + +init(wrap=False) + +TRACE_LEVEL_NUM: int = 5 +SUCCESS_LEVEL_NUM: int = 21 + + +def _trace(self, message: str, *args, **kws): + if self.isEnabledFor(TRACE_LEVEL_NUM): + self._log(TRACE_LEVEL_NUM, message, args, **kws) + + +def _success(self, message: str, *args, **kws): + if self.isEnabledFor(SUCCESS_LEVEL_NUM): + self._log(SUCCESS_LEVEL_NUM, message, args, **kws) + + +logging.SUCCESS = SUCCESS_LEVEL_NUM +logging.addLevelName(SUCCESS_LEVEL_NUM, "SUCCESS") +logging.Logger.success = _success + +logging.TRACE = TRACE_LEVEL_NUM +logging.addLevelName(TRACE_LEVEL_NUM, "TRACE") +logging.Logger.trace = _trace + +emoji_map: dict[str, str] = { + ":white_heavy_check_mark:": "✅", + ":cross_mark:": "❌", + ":satellite:": "🛰️", + ":warning:": "⚠️", + ":arrow_right:": "➡️", + ":hourglass:": "⏳", +} + + +color_map: dict[str, str] = { + "[red]": Fore.RED, + "[/red]": Style.RESET_ALL, + "[blue]": Fore.BLUE, + "[/blue]": Style.RESET_ALL, + "[green]": Fore.GREEN, + "[/green]": Style.RESET_ALL, + "[magenta]": Fore.MAGENTA, + "[/magenta]": Style.RESET_ALL, + "[yellow]": Fore.YELLOW, + "[/yellow]": Style.RESET_ALL, + "[orange]": Fore.YELLOW, + "[/orange]": Style.RESET_ALL, +} + + +log_level_color_prefix: dict[int, str] = { + logging.NOTSET: Fore.RESET, + logging.TRACE: Fore.MAGENTA, + logging.DEBUG: Fore.BLUE, + logging.INFO: Fore.WHITE, + logging.SUCCESS: Fore.GREEN, + logging.WARNING: Fore.YELLOW, + logging.ERROR: Fore.RED, + logging.CRITICAL: Back.RED, +} + + +LOG_FORMATS: dict[int, str] = { + level: f"{Fore.BLUE}%(asctime)s{Fore.RESET} | {Style.BRIGHT}{color}%(levelname)s\033[0m | %(message)s" + for level, color in log_level_color_prefix.items() +} + +LOG_TRACE_FORMATS: dict[int, str] = { + level: f"{Fore.BLUE}%(asctime)s{Fore.RESET}" + f" | {Style.BRIGHT}{color}%(levelname)s{Fore.RESET}{Back.RESET}{Style.RESET_ALL}" + f" | %(name)s:%(filename)s:%(lineno)s" + f" | %(message)s" + for level, color in log_level_color_prefix.items() +} + +DEFAULT_LOG_FORMAT: str = ( + f"{Fore.BLUE}%(asctime)s{Fore.RESET} | " + f"{Style.BRIGHT}{Fore.WHITE}%(levelname)s{Style.RESET_ALL} | " + f"%(name)s:%(filename)s:%(lineno)s | %(message)s" +) + +DEFAULT_TRACE_FORMAT: str = ( + f"{Fore.BLUE}%(asctime)s{Fore.RESET} | " + f"{Style.BRIGHT}{Fore.WHITE}%(levelname)s{Style.RESET_ALL} | " + f"%(name)s:%(filename)s:%(lineno)s | %(message)s" +) + + +class BtStreamFormatter(logging.Formatter): + """ + A custom logging formatter for the Bittensor project that overrides the time formatting to include milliseconds, + centers the level name, and applies custom log formats, emojis, and colors. + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.trace = False + + def formatTime(self, record, datefmt: Optional[str] = None) -> str: + """ + Override formatTime to add milliseconds. + + Args: + record (logging.LogRecord): The log record. + datefmt (Optional[str]): The date format string. + + Returns: + s (str): The formatted time string with milliseconds. + """ + + created = self.converter(record.created) + if datefmt: + s = time.strftime(datefmt, created) + else: + s = time.strftime("%Y-%m-%d %H:%M:%S", created) + s += f".{int(record.msecs):03d}" + return s + + def format(self, record: "logging.LogRecord") -> str: + """ + Override format to apply custom formatting including emojis and colors. + + This method saves the original format, applies custom formatting based on the log level and trace flag, replaces + text with emojis and colors, and then returns the formatted log record. + + Args: + record (logging.LogRecord): The log record. + + Returns: + result (str): The formatted log record. + """ + + format_orig = self._style._fmt + record.levelname = f"{record.levelname:^16}" + + if record.levelno not in LOG_FORMATS: + self._style._fmt = ( + DEFAULT_TRACE_FORMAT if self.trace else DEFAULT_LOG_FORMAT + ) + else: + if self.trace is True: + self._style._fmt = LOG_TRACE_FORMATS[record.levelno] + else: + self._style._fmt = LOG_FORMATS[record.levelno] + + for text, emoji in emoji_map.items(): + record.msg = record.msg.replace(text, emoji) + # Apply color specifiers + for text, color in color_map.items(): + record.msg = record.msg.replace(text, color) + + result = super().format(record) + self._style._fmt = format_orig + + return result + + def set_trace(self, state: bool = True): + """Change formatter state.""" + self.trace = state + + +class BtFileFormatter(logging.Formatter): + """ + BtFileFormatter + + A custom logging formatter for the Bittensor project that overrides the time formatting to include milliseconds and + centers the level name. + """ + + def formatTime( + self, record: "logging.LogRecord", datefmt: Optional[str] = None + ) -> str: + """ + Override formatTime to add milliseconds. + + Args: + record (logging.LogRecord): The log record. + datefmt (Optional[str]): The date format string. + + Returns: + s (str): The formatted time string with milliseconds. + """ + + created = self.converter(record.created) + if datefmt: + s = time.strftime(datefmt, created) + else: + s = time.strftime("%Y-%m-%d %H:%M:%S", created) + s += f".{int(record.msecs):03d}" + return s + + def format(self, record: "logging.LogRecord") -> str: + """ + Override format to center the level name. + + Args: + record (logging.LogRecord): The log record. + + Returns: + formatted record (str): The formatted log record. + """ + record.levelname = f"{record.levelname:^10}" + return super().format(record) diff --git a/bittensor/utils/btlogging/helpers.py b/bittensor/utils/btlogging/helpers.py new file mode 100644 index 0000000000..266a67b25d --- /dev/null +++ b/bittensor/utils/btlogging/helpers.py @@ -0,0 +1,71 @@ +""" +btlogging.helpers module provides helper functions for the Bittensor logging system. +""" + +import logging +from typing import Generator + + +def all_loggers() -> Generator["logging.Logger", None, None]: + """Generator that yields all logger instances in the application. + + Iterates through the logging root manager's logger dictionary and yields all active `Logger` instances. It skips + placeholders and other types that are not instances of `Logger`. + + Yields: + logger (logging.Logger): An active logger instance. + """ + for logger in logging.root.manager.loggerDict.values(): + if isinstance(logger, logging.PlaceHolder): + continue + # In some versions of Python, the values in loggerDict might be + # LoggerAdapter instances instead of Logger instances. + # We check for Logger instances specifically. + if isinstance(logger, logging.Logger): + yield logger + else: + # If it's not a Logger instance, it could be a LoggerAdapter or + # another form that doesn't directly offer logging methods. + # This branch can be extended to handle such cases as needed. + pass + + +def all_logger_names() -> Generator[str, None, None]: + """ + Generate the names of all active loggers. + + This function iterates through the logging root manager's logger dictionary and yields the names of all active + `Logger` instances. It skips placeholders and other types that are not instances of `Logger`. + + Yields: + name (str): The name of an active logger. + """ + for name, logger in logging.root.manager.loggerDict.items(): + if isinstance(logger, logging.PlaceHolder): + continue + # In some versions of Python, the values in loggerDict might be + # LoggerAdapter instances instead of Logger instances. + # We check for Logger instances specifically. + if isinstance(logger, logging.Logger): + yield name + else: + # If it's not a Logger instance, it could be a LoggerAdapter or + # another form that doesn't directly offer logging methods. + # This branch can be extended to handle such cases as needed. + pass + + +def get_max_logger_name_length() -> int: + """ + Calculate and return the length of the longest logger name. + + This function iterates through all active logger names and determines the length of the longest name. + + Returns: + max_length (int): The length of the longest logger name. + """ + max_length = 0 + for name in all_logger_names(): + if len(name) > max_length: + max_length = len(name) + return max_length diff --git a/bittensor/utils/btlogging/loggingmachine.py b/bittensor/utils/btlogging/loggingmachine.py new file mode 100644 index 0000000000..32ea7315e0 --- /dev/null +++ b/bittensor/utils/btlogging/loggingmachine.py @@ -0,0 +1,669 @@ +""" +Module provides a logging framework for Bittensor, managing both Bittensor-specific and third-party logging states. +It leverages the StateMachine from the statemachine package to transition between different logging states such as +Default, Debug, Trace, and Disabled. +""" + +import argparse +import atexit +import logging as stdlogging +import multiprocessing as mp +import os +import sys +from logging import Logger +from logging.handlers import QueueHandler, QueueListener, RotatingFileHandler +from typing import NamedTuple + +from statemachine import State, StateMachine + +from bittensor.core.settings import READ_ONLY +from bittensor.core.config import Config +from bittensor.utils.btlogging.console import BittensorConsole +from .defines import ( + BITTENSOR_LOGGER_NAME, + DATE_FORMAT, + DEFAULT_LOG_BACKUP_COUNT, + DEFAULT_LOG_FILE_NAME, + DEFAULT_MAX_ROTATING_LOG_FILE_SIZE, + TRACE_LOG_FORMAT, +) +from .format import BtFileFormatter, BtStreamFormatter +from .helpers import all_loggers + +# https://github.com/python/cpython/issues/97941 +CUSTOM_LOGGER_METHOD_STACK_LEVEL = 2 if sys.version_info >= (3, 11) else 1 + + +def _concat_message(msg="", prefix="", suffix=""): + """Concatenates a message with optional prefix and suffix.""" + message_parts = [ + str(component).strip() + for component in [prefix, msg, suffix] + if component is not None and str(component).strip() + ] + formatted_message = " - ".join(message_parts) + return formatted_message + + +class LoggingConfig(NamedTuple): + """Named tuple to hold the logging configuration.""" + + debug: bool + trace: bool + info: bool + record_log: bool + logging_dir: str + + +class LoggingMachine(StateMachine, Logger): + """Handles logger states for bittensor and 3rd party libraries.""" + + Default = State(initial=True) + Debug = State() + Trace = State() + Disabled = State() + Warning = State() + Info = State() + + enable_default = ( + Debug.to(Default) + | Trace.to(Default) + | Disabled.to(Default) + | Default.to(Default) + | Warning.to(Default) + | Info.to(Default) + ) + + enable_console = ( + Default.to(Debug) + | Trace.to(Debug) + | Disabled.to(Debug) + | Debug.to(Debug) + | Warning.to(Debug) + | Info.to(Debug) + ) + + enable_info = ( + Default.to(Info) + | Debug.to(Info) + | Trace.to(Info) + | Disabled.to(Info) + | Warning.to(Info) + | Info.to(Info) + ) + + enable_trace = ( + Default.to(Trace) + | Debug.to(Trace) + | Disabled.to(Trace) + | Trace.to(Trace) + | Warning.to(Trace) + | Info.to(Trace) + ) + + enable_debug = ( + Default.to(Debug) + | Trace.to(Debug) + | Disabled.to(Debug) + | Debug.to(Debug) + | Warning.to(Debug) + | Info.to(Debug) + ) + + enable_warning = ( + Default.to(Warning) + | Trace.to(Warning) + | Disabled.to(Warning) + | Debug.to(Warning) + | Warning.to(Warning) + | Info.to(Warning) + ) + + disable_trace = Trace.to(Default) + + disable_debug = Debug.to(Default) + + disable_warning = Warning.to(Default) + + disable_info = Info.to(Default) + + disable_logging = ( + Trace.to(Disabled) + | Debug.to(Disabled) + | Default.to(Disabled) + | Disabled.to(Disabled) + | Info.to(Disabled) + ) + + def __init__(self, config: "Config", name: str = BITTENSOR_LOGGER_NAME): + # basics + StateMachine.__init__(self) + stdlogging.Logger.__init__(self, name) + self._queue = mp.Queue(-1) + self._primary_loggers = {name} + self._config = self._extract_logging_config(config) + + # Formatters + # + # In the future, this may be expanded to a dictionary mapping handler + # types to their respective formatters. + self._stream_formatter = BtStreamFormatter() + self._file_formatter = BtFileFormatter(TRACE_LOG_FORMAT, DATE_FORMAT) + + # start with handlers for the QueueListener. + # + # In the future, we may want to add options to introduce other handlers + # for things like log aggregation by external services. + self._handlers = self._configure_handlers(self._config) + + # configure and start the queue listener + self._listener = self._create_and_start_listener(self._handlers) + + # set up all the loggers + self._logger = self._initialize_bt_logger(name) + self.disable_third_party_loggers() + self._enable_initial_state(self._config) + self.console = BittensorConsole(self) + + def _enable_initial_state(self, config): + """Set correct state action on initializing""" + if config.trace: + self.enable_trace() + elif config.debug: + self.enable_debug() + elif config.info: + self.enable_info() + else: + self.enable_default() + + def _extract_logging_config(self, config: "Config") -> dict: + """Extract btlogging's config from bittensor config + + Args: + config (bittensor.core.config.Config): Bittensor config instance. + + Returns: + (dict): btlogging's config from Bittensor config or Bittensor config. + """ + # This is to handle nature of DefaultMunch + if getattr(config, "logging", None): + return config.logging + else: + return config + + def _configure_handlers(self, config) -> list[stdlogging.Handler]: + handlers = list() + + # stream handler, a given + stream_handler = stdlogging.StreamHandler(sys.stdout) + stream_handler.setFormatter(self._stream_formatter) + handlers.append(stream_handler) + + # file handler, maybe + if config.record_log and config.logging_dir: + logfile = os.path.abspath( + os.path.join(config.logging_dir, DEFAULT_LOG_FILE_NAME) + ) + file_handler = self._create_file_handler(logfile) + handlers.append(file_handler) + return handlers + + def get_config(self): + return self._config + + def set_config(self, config: "Config"): + """Set config after initialization, if desired. + + Args: + config (bittensor.core.config.Config): Bittensor config instance. + """ + self._config = self._extract_logging_config(config) + if self._config.logging_dir and self._config.record_log: + expanded_dir = os.path.expanduser(config.logging_dir) + logfile = os.path.abspath(os.path.join(expanded_dir, DEFAULT_LOG_FILE_NAME)) + self._enable_file_logging(logfile) + if self._config.trace: + self.enable_trace() + elif self._config.debug: + self.enable_debug() + elif self._config.info: + self.enable_info() + + def _create_and_start_listener(self, handlers): + """ + A listener to receive and publish log records. + + This listener receives records from a queue populated by the main bittensor logger, as well as 3rd party loggers + """ + + listener = QueueListener(self._queue, *handlers, respect_handler_level=True) + listener.start() + atexit.register(listener.stop) + return listener + + def get_queue(self): + """ + Get the queue the QueueListener is publishing from. + + To set up logging in a separate process, a QueueHandler must be added to all the desired loggers. + """ + return self._queue + + def _initialize_bt_logger(self, name: str): + """ + Initialize logging for bittensor. + + Since the initial state is Default, logging level for the module logger is INFO, and all third-party loggers are + silenced. Subsequent state transitions will handle all logger outputs. + """ + logger = stdlogging.getLogger(name) + queue_handler = QueueHandler(self._queue) + logger.addHandler(queue_handler) + return logger + + def _deinitialize_bt_logger(self, name: str): + """Find the logger by name and remove the queue handler associated with it.""" + logger = stdlogging.getLogger(name) + for handler in list(logger.handlers): + if isinstance(handler, QueueHandler): + logger.removeHandler(handler) + return logger + + def _create_file_handler(self, logfile: str): + file_handler = RotatingFileHandler( + logfile, + maxBytes=DEFAULT_MAX_ROTATING_LOG_FILE_SIZE, + backupCount=DEFAULT_LOG_BACKUP_COUNT, + ) + file_handler.setFormatter(self._file_formatter) + file_handler.setLevel(stdlogging.TRACE) + return file_handler + + def register_primary_logger(self, name: str): + """ + Register a logger as primary logger + + This adds a logger to the _primary_loggers set to ensure + it doesn't get disabled when disabling third-party loggers. + A queue handler is also associated with it. + + Args: + name (str): the name for primary logger. + """ + self._primary_loggers.add(name) + self._initialize_bt_logger(name) + + def deregister_primary_logger(self, name: str): + """ + De-registers a primary logger + + This function removes the logger from the _primary_loggers + set and deinitializes its queue handler + + Args: + name (str): the name of primary logger. + """ + self._primary_loggers.remove(name) + self._deinitialize_bt_logger(name) + + def enable_third_party_loggers(self): + """Enables logging for third-party loggers by adding a queue handler to each.""" + for logger in all_loggers(): + if logger.name in self._primary_loggers: + continue + queue_handler = QueueHandler(self._queue) + logger.addHandler(queue_handler) + logger.setLevel(self._logger.level) + + def disable_third_party_loggers(self): + """Disables logging for third-party loggers by removing all their handlers.""" + # remove all handlers + for logger in all_loggers(): + if logger.name in self._primary_loggers: + continue + for handler in logger.handlers: + logger.removeHandler(handler) + + def _enable_file_logging(self, logfile: str): + # preserve idempotency; do not create extra filehandlers + # if one already exists + if any( + [isinstance(handler, RotatingFileHandler) for handler in self._handlers] + ): + return + file_handler = self._create_file_handler(logfile) + self._handlers.append(file_handler) + self._listener.handlers = tuple(self._handlers) + + # state transitions + def before_transition(self, event, state): + """Stops listener after transition.""" + self._listener.stop() + + def after_transition(self, event, state): + """Starts listener after transition.""" + self._listener.start() + + # Default Logging + def before_enable_default(self): + """Logs status before enable Default.""" + self._logger.info("Enabling default logging (Warning level)") + self._logger.setLevel(stdlogging.WARNING) + for logger in all_loggers(): + if logger.name in self._primary_loggers: + continue + logger.setLevel(stdlogging.CRITICAL) + + def after_enable_default(self): + pass + + # Warning + def before_enable_warning(self): + """Logs status before enable Warning.""" + self._logger.info("Enabling warning.") + self._stream_formatter.set_trace(True) + for logger in all_loggers(): + logger.setLevel(stdlogging.WARNING) + + def after_enable_warning(self): + """Logs status after enable Warning.""" + self._logger.info("Warning enabled.") + + # Info + def before_enable_info(self): + """Logs status before enable info.""" + self._logger.info("Enabling info logging.") + self._logger.setLevel(stdlogging.INFO) + for logger in all_loggers(): + if logger.name in self._primary_loggers: + continue + logger.setLevel(stdlogging.INFO) + + def after_enable_info(self): + """Logs status after enable info.""" + self._logger.info("Info enabled.") + + # Trace + def before_enable_trace(self): + """Logs status before enable Trace.""" + self._logger.info("Enabling trace.") + self._stream_formatter.set_trace(True) + for logger in all_loggers(): + logger.setLevel(stdlogging.TRACE) + + def after_enable_trace(self): + """Logs status after enable Trace.""" + self._logger.info("Trace enabled.") + + def before_disable_trace(self): + """Logs status before disable Trace.""" + self._logger.info("Disabling trace.") + self._stream_formatter.set_trace(False) + self.enable_default() + + def after_disable_trace(self): + """Logs status after disable Trace.""" + self._logger.info("Trace disabled.") + + # Debug + def before_enable_debug(self): + """Logs status before enable Debug.""" + self._logger.info("Enabling debug.") + self._stream_formatter.set_trace(True) + for logger in all_loggers(): + logger.setLevel(stdlogging.DEBUG) + + def before_enable_console(self): + """Logs status before enable Console.""" + self._stream_formatter.set_trace(True) + for logger in all_loggers(): + logger.setLevel(stdlogging.DEBUG) + + def after_enable_debug(self): + """Logs status after enable Debug.""" + self._logger.info("Debug enabled.") + + def before_disable_debug(self): + """Logs status before disable Debug.""" + self._logger.info("Disabling debug.") + self._stream_formatter.set_trace(False) + self.enable_default() + + def after_disable_debug(self): + """Logs status after disable Debug.""" + self._logger.info("Debug disabled.") + + # Disable Logging + def before_disable_logging(self): + """ + Prepares the logging system for disabling. + + This method performs the following actions: + 1. Logs an informational message indicating that logging is being disabled. + 2. Disables trace mode in the stream formatter. + 3. Sets the logging level to CRITICAL for all loggers. + + This ensures that only critical messages will be logged after this method is called. + """ + self._logger.info("Disabling logging.") + self._stream_formatter.set_trace(False) + + for logger in all_loggers(): + logger.setLevel(stdlogging.CRITICAL) + + # Required API support log commands for API backwards compatibility. + @property + def __trace_on__(self) -> bool: + """ + Checks if the current state is in "Trace" mode. + + Returns: + bool: True if the current state is "Trace", otherwise False. + """ + return self.current_state_value == "Trace" + + def trace(self, msg="", prefix="", suffix="", *args, stacklevel=1, **kwargs): + """Wraps trace message with prefix and suffix.""" + msg = _concat_message(msg, prefix, suffix) + self._logger.trace( + msg, + *args, + **kwargs, + stacklevel=stacklevel + CUSTOM_LOGGER_METHOD_STACK_LEVEL, + ) + + def debug(self, msg="", prefix="", suffix="", *args, stacklevel=1, **kwargs): + """Wraps debug message with prefix and suffix.""" + msg = _concat_message(msg, prefix, suffix) + self._logger.debug(msg, *args, **kwargs, stacklevel=stacklevel + 1) + + def info(self, msg="", prefix="", suffix="", *args, stacklevel=1, **kwargs): + """Wraps info message with prefix and suffix.""" + msg = _concat_message(msg, prefix, suffix) + self._logger.info(msg, *args, **kwargs, stacklevel=stacklevel + 1) + + def success(self, msg="", prefix="", suffix="", *args, stacklevel=1, **kwargs): + """Wraps success message with prefix and suffix.""" + msg = _concat_message(msg, prefix, suffix) + self._logger.success( + msg, + *args, + **kwargs, + stacklevel=stacklevel + CUSTOM_LOGGER_METHOD_STACK_LEVEL, + ) + + def warning(self, msg="", prefix="", suffix="", *args, stacklevel=1, **kwargs): + """Wraps warning message with prefix and suffix.""" + msg = _concat_message(msg, prefix, suffix) + self._logger.warning(msg, *args, **kwargs, stacklevel=stacklevel + 1) + + def error(self, msg="", prefix="", suffix="", *args, stacklevel=1, **kwargs): + """Wraps error message with prefix and suffix.""" + msg = _concat_message(msg, prefix, suffix) + self._logger.error(msg, *args, **kwargs, stacklevel=stacklevel + 1) + + def critical(self, msg="", prefix="", suffix="", *args, stacklevel=1, **kwargs): + """Wraps critical message with prefix and suffix.""" + msg = _concat_message(msg, prefix, suffix) + self._logger.critical(msg, *args, **kwargs, stacklevel=stacklevel + 1) + + def exception(self, msg="", prefix="", suffix="", *args, stacklevel=1, **kwargs): + """Wraps exception message with prefix and suffix.""" + msg = _concat_message(msg, prefix, suffix) + self._logger.exception(msg, *args, **kwargs, stacklevel=stacklevel + 1) + + def on(self): + """Enable default state.""" + self._logger.info("Logging enabled.") + self.enable_default() + + def off(self): + """Disables all states.""" + self.disable_logging() + + def set_debug(self, on: bool = True): + """Sets Debug state.""" + if on and not self.current_state_value == "Debug": + self.enable_debug() + elif not on: + if self.current_state_value == "Debug": + self.disable_debug() + + def set_trace(self, on: bool = True): + """Sets Trace state.""" + if on and not self.current_state_value == "Trace": + self.enable_trace() + elif not on: + if self.current_state_value == "Trace": + self.disable_trace() + + def set_info(self, on: bool = True): + """Sets Info state.""" + if on and not self.current_state_value == "Info": + self.enable_info() + elif not on: + if self.current_state_value == "Info": + self.disable_info() + + def set_warning(self, on: bool = True): + """Sets Warning state.""" + if on and not self.current_state_value == "Warning": + self.enable_warning() + elif not on: + if self.current_state_value == "Warning": + self.disable_warning() + + def set_default(self): + """Sets Default state.""" + if not self.current_state_value == "Default": + self.enable_default() + + def set_console(self): + """Sets Console state.""" + if not self.current_state_value == "Console": + self.enable_console() + + def get_level(self) -> int: + """Returns Logging level.""" + return self._logger.level + + def setLevel(self, level): + """Set the specified level on the underlying logger.""" + self._logger.setLevel(level) + + def check_config(self, config: "Config"): + assert config.logging + + def help(self): + pass + + @classmethod + def add_args(cls, parser: argparse.ArgumentParser, prefix: str = None): + """Accept specific arguments fro parser""" + prefix_str = "" if prefix is None else prefix + "." + try: + default_logging_debug = os.getenv("BT_LOGGING_DEBUG") or False + default_logging_info = os.getenv("BT_LOGGING_INFO") or False + default_logging_trace = os.getenv("BT_LOGGING_TRACE") or False + default_logging_record_log = os.getenv("BT_LOGGING_RECORD_LOG") or False + default_logging_logging_dir = ( + None + if READ_ONLY + else os.getenv("BT_LOGGING_LOGGING_DIR") + or os.path.join("~", ".bittensor", "miners") + ) + parser.add_argument( + "--" + prefix_str + "logging.debug", + action="store_true", + help="""Turn on bittensor debugging information""", + default=default_logging_debug, + ) + parser.add_argument( + "--" + prefix_str + "logging.trace", + action="store_true", + help="""Turn on bittensor trace level information""", + default=default_logging_trace, + ) + parser.add_argument( + "--" + prefix_str + "logging.info", + action="store_true", + help="""Turn on bittensor info level information""", + default=default_logging_info, + ) + parser.add_argument( + "--" + prefix_str + "logging.record_log", + action="store_true", + help="""Turns on logging to file.""", + default=default_logging_record_log, + ) + parser.add_argument( + "--" + prefix_str + "logging.logging_dir", + type=str, + help="Logging default root directory.", + default=default_logging_logging_dir, + ) + except argparse.ArgumentError: + # re-parsing arguments. + pass + + @classmethod + def config(cls) -> "Config": + """Get config from the argument parser. + + Return: + config (bittensor.core.config.Config): config object + """ + parser = argparse.ArgumentParser() + cls.add_args(parser) + return Config(parser) + + def __call__( + self, + config: "Config" = None, + debug: bool = None, + trace: bool = None, + info: bool = None, + record_log: bool = None, + logging_dir: str = None, + ): + if config is not None: + cfg = self._extract_logging_config(config) + if info is not None: + cfg.info = info + elif debug is not None: + cfg.debug = debug + elif trace is not None: + cfg.trace = trace + if record_log is not None: + cfg.record_log = record_log + if logging_dir is not None: + cfg.logging_dir = logging_dir + else: + cfg = LoggingConfig( + debug=debug, + trace=trace, + info=info, + record_log=record_log, + logging_dir=logging_dir, + ) + self.set_config(cfg) diff --git a/bittensor/utils/certifi.sh b/bittensor/utils/certifi.sh new file mode 100755 index 0000000000..9dd4a68fe3 --- /dev/null +++ b/bittensor/utils/certifi.sh @@ -0,0 +1,34 @@ +#!/bin/bash + +# Locate Python 3 +PYTHON=$(command -v python3) +if [ -z "$PYTHON" ]; then + echo "Error: Python 3 is not installed or not found in PATH." + exit 1 +fi + +echo "Using Python: $PYTHON" + +echo " -- Upgrading the certifi package" +$PYTHON -m pip install --upgrade certifi + +echo " -- Fetching the path to the certifi certificate bundle" +CERTIFI_CAFILE=$($PYTHON -c "import certifi; print(certifi.where())") + +echo " -- Resolving OpenSSL directory and certificate file path" +OPENSSL_DIR=$($PYTHON -c "import ssl; print(ssl.get_default_verify_paths().openssl_cafile.rsplit('/', 1)[0])") +OPENSSL_CAFILE=$($PYTHON -c "import ssl; print(ssl.get_default_verify_paths().openssl_cafile.rsplit('/', 1)[-1])") + +echo " -- Navigating to the OpenSSL directory" +cd "$OPENSSL_DIR" || { echo "Failed to navigate to $OPENSSL_DIR"; exit 1; } + +echo " -- Removing any existing certificate file or symlink" +rm -f "$OPENSSL_CAFILE" + +echo " -- Creating a symlink to the certifi certificate bundle" +ln -s "$CERTIFI_CAFILE" "$OPENSSL_CAFILE" + +echo " -- Setting appropriate file permissions" +chmod 775 "$OPENSSL_CAFILE" + +echo " -- Update complete" diff --git a/bittensor/utils/easy_imports.py b/bittensor/utils/easy_imports.py new file mode 100644 index 0000000000..a2645b0668 --- /dev/null +++ b/bittensor/utils/easy_imports.py @@ -0,0 +1,302 @@ +""" +The Bittensor Compatibility Module is designed to ensure seamless integration and functionality with legacy versions of +the Bittensor framework, specifically up to and including version 7.3.0. This module addresses changes and deprecated +features in recent versions, allowing users to maintain compatibility with older systems and projects. +""" + +import importlib +import sys + +from bittensor_wallet import Keypair +from bittensor_wallet.errors import KeyFileError +from bittensor_wallet.keyfile import ( + serialized_keypair_to_keyfile_data, + deserialize_keypair_from_keyfile_data, + validate_password, + ask_password_to_encrypt, + keyfile_data_is_encrypted_nacl, + keyfile_data_is_encrypted_ansible, + keyfile_data_is_encrypted_legacy, + keyfile_data_is_encrypted, + keyfile_data_encryption_method, + legacy_encrypt_keyfile_data, + encrypt_keyfile_data, + get_coldkey_password_from_environment, + decrypt_keyfile_data, + Keyfile, +) +from bittensor_wallet.wallet import display_mnemonic_msg, Wallet + +from bittensor.core import settings, timelock +from bittensor.core.async_subtensor import AsyncSubtensor +from bittensor.core.axon import Axon +from bittensor.core.chain_data import ( + AxonInfo, + ChainIdentity, + DelegateInfo, + DelegateInfoLite, + DynamicInfo, + IPInfo, + MetagraphInfo, + MetagraphInfoEmissions, + MetagraphInfoParams, + MetagraphInfoPool, + NeuronInfo, + NeuronInfoLite, + PrometheusInfo, + ProposalCallData, + ProposalVoteData, + ScheduledColdkeySwapInfo, + SelectiveMetagraphIndex, + StakeInfo, + SubnetHyperparameters, + SubnetIdentity, + SubnetInfo, + SubnetState, + WeightCommitInfo, +) +from bittensor.core.config import Config +from bittensor.core.dendrite import Dendrite +from bittensor.core.errors import ( + BlacklistedException, + ChainConnectionError, + ChainError, + ChainQueryError, + ChainTransactionError, + DelegateTakeTooHigh, + DelegateTakeTooLow, + DelegateTxRateLimitExceeded, + DuplicateChild, + HotKeyAccountNotExists, + IdentityError, + InternalServerError, + InvalidChild, + InvalidRequestNameError, + MetadataError, + NominationError, + NonAssociatedColdKey, + NotDelegateError, + NotEnoughStakeToSetChildkeys, + NotRegisteredError, + NotVerifiedException, + PostProcessException, + PriorityException, + ProportionOverflow, + RegistrationError, + RegistrationNotPermittedOnRootSubnet, + RunException, + StakeError, + SubNetworkDoesNotExist, + SynapseDendriteNoneException, + SynapseParsingError, + TooManyChildren, + TransferError, + TxRateLimitExceeded, + UnknownSynapseError, + UnstakeError, +) +from bittensor.core.metagraph import Metagraph +from bittensor.core.settings import BLOCKTIME +from bittensor.core.stream import StreamingSynapse +from bittensor.core.subtensor import Subtensor +from bittensor.core.subtensor_api import SubtensorApi +from bittensor.core.synapse import TerminalInfo, Synapse +from bittensor.core.tensor import Tensor +from bittensor.core.threadpool import PriorityThreadPoolExecutor +from bittensor.utils import ( + ss58_to_vec_u8, + version_checking, + strtobool, + get_explorer_url_for_network, + ss58_address_to_bytes, + u16_normalized_float, + u64_normalized_float, + get_hash, +) +from bittensor.utils.balance import Balance +from bittensor.utils.balance import tao, rao +from bittensor.utils.btlogging import logging +from bittensor.utils.mock.subtensor_mock import MockSubtensor +from bittensor.utils.subnets import SubnetsAPI + + +# Backwards compatibility with previous bittensor versions. +async_subtensor = AsyncSubtensor +axon = Axon +config = Config +dendrite = Dendrite +keyfile = Keyfile +metagraph = Metagraph +wallet = Wallet +subtensor = Subtensor +synapse = Synapse + +# Makes the `bittensor.utils.mock` subpackage available as `bittensor.mock` for backwards compatibility. +mock_subpackage = importlib.import_module("bittensor.utils.mock") +sys.modules["bittensor.mock"] = mock_subpackage + +# Makes the `bittensor.core.extrinsics` subpackage available as `bittensor.extrinsics` for backwards compatibility. +extrinsics_subpackage = importlib.import_module("bittensor.core.extrinsics") +sys.modules["bittensor.extrinsics"] = extrinsics_subpackage + + +# Logging helpers. +def trace(on: bool = True): + """ + Enables or disables trace logging. + Args: + on (bool): If True, enables trace logging. If False, disables trace logging. + """ + logging.set_trace(on) + + +def debug(on: bool = True): + """ + Enables or disables debug logging. + Args: + on (bool): If True, enables debug logging. If False, disables debug logging. + """ + logging.set_debug(on) + + +def warning(on: bool = True): + """ + Enables or disables warning logging. + Args: + on (bool): If True, enables warning logging. If False, disables warning logging and sets default (WARNING) level. + """ + logging.set_warning(on) + + +def info(on: bool = True): + """ + Enables or disables info logging. + Args: + on (bool): If True, enables info logging. If False, disables info logging and sets default (WARNING) level. + """ + logging.set_info(on) + + +__all__ = [ + "Keypair", + "KeyFileError", + "serialized_keypair_to_keyfile_data", + "deserialize_keypair_from_keyfile_data", + "validate_password", + "ask_password_to_encrypt", + "keyfile_data_is_encrypted_nacl", + "keyfile_data_is_encrypted_ansible", + "keyfile_data_is_encrypted_legacy", + "keyfile_data_is_encrypted", + "keyfile_data_encryption_method", + "legacy_encrypt_keyfile_data", + "encrypt_keyfile_data", + "get_coldkey_password_from_environment", + "decrypt_keyfile_data", + "Keyfile", + "display_mnemonic_msg", + "Wallet", + "settings", + "timelock", + "AsyncSubtensor", + "Axon", + "AxonInfo", + "ChainIdentity", + "DelegateInfo", + "DelegateInfoLite", + "DynamicInfo", + "IPInfo", + "MetagraphInfo", + "MetagraphInfoEmissions", + "MetagraphInfoParams", + "MetagraphInfoPool", + "NeuronInfo", + "NeuronInfoLite", + "PrometheusInfo", + "ProposalCallData", + "ProposalVoteData", + "ScheduledColdkeySwapInfo", + "SelectiveMetagraphIndex", + "StakeInfo", + "SubnetHyperparameters", + "SubnetIdentity", + "SubnetInfo", + "SubnetState", + "WeightCommitInfo", + "Config", + "Dendrite", + "BlacklistedException", + "ChainConnectionError", + "ChainError", + "ChainQueryError", + "ChainTransactionError", + "DelegateTakeTooHigh", + "DelegateTakeTooLow", + "DelegateTxRateLimitExceeded", + "DuplicateChild", + "HotKeyAccountNotExists", + "IdentityError", + "InternalServerError", + "InvalidChild", + "InvalidRequestNameError", + "MetadataError", + "NominationError", + "NonAssociatedColdKey", + "NotDelegateError", + "NotEnoughStakeToSetChildkeys", + "NotRegisteredError", + "NotVerifiedException", + "PostProcessException", + "PriorityException", + "ProportionOverflow", + "RegistrationError", + "RegistrationNotPermittedOnRootSubnet", + "RunException", + "StakeError", + "SubNetworkDoesNotExist", + "SynapseDendriteNoneException", + "SynapseParsingError", + "TooManyChildren", + "TransferError", + "TxRateLimitExceeded", + "UnknownSynapseError", + "UnstakeError", + "Metagraph", + "BLOCKTIME", + "StreamingSynapse", + "Subtensor", + "SubtensorApi", + "TerminalInfo", + "Synapse", + "Tensor", + "PriorityThreadPoolExecutor", + "ss58_to_vec_u8", + "version_checking", + "strtobool", + "get_explorer_url_for_network", + "ss58_address_to_bytes", + "u16_normalized_float", + "u64_normalized_float", + "get_hash", + "Balance", + "tao", + "rao", + "logging", + "MockSubtensor", + "SubnetsAPI", + "async_subtensor", + "axon", + "config", + "dendrite", + "keyfile", + "metagraph", + "wallet", + "subtensor", + "synapse", + "trace", + "debug", + "warning", + "info", + "mock_subpackage", + "extrinsics_subpackage", +] diff --git a/bittensor/utils/formatting.py b/bittensor/utils/formatting.py new file mode 100644 index 0000000000..02023e189e --- /dev/null +++ b/bittensor/utils/formatting.py @@ -0,0 +1,24 @@ +import math + + +def get_human_readable(num, suffix="H"): + """Convert a number into a human-readable format with suffixes.""" + for unit in ["", "K", "M", "G", "T", "P", "E", "Z"]: + if abs(num) < 1000.0: + return f"{num:3.1f}{unit}{suffix}" + num /= 1000.0 + return f"{num:.1f}Y{suffix}" + + +def millify(n: int): + """Converts a number into a more readable format with suffixes.""" + mill_names = ["", " K", " M", " B", " T"] + n = float(n) + mill_idx = max( + 0, + min( + len(mill_names) - 1, + int(math.floor(0 if n == 0 else math.log10(abs(n)) / 3)), + ), + ) + return "{:.2f}{}".format(n / 10 ** (3 * mill_idx), mill_names[mill_idx]) diff --git a/bittensor/utils/liquidity.py b/bittensor/utils/liquidity.py new file mode 100644 index 0000000000..55e206225c --- /dev/null +++ b/bittensor/utils/liquidity.py @@ -0,0 +1,158 @@ +""" +This module provides utilities for managing liquidity positions and price conversions in the Bittensor network. The +module handles conversions between TAO and Alpha tokens while maintaining precise calculations for liquidity +provisioning and fee distribution. +""" + +import math +from typing import Any +from dataclasses import dataclass + +from bittensor.utils.balance import Balance, fixed_to_float + +# These three constants are unchangeable at the level of Uniswap math +MIN_TICK = -887272 +MAX_TICK = 887272 +PRICE_STEP = 1.0001 + + +@dataclass +class LiquidityPosition: + id: int + price_low: Balance # RAO + price_high: Balance # RAO + liquidity: Balance # TAO + ALPHA (sqrt by TAO balance * Alpha Balance -> math under the hood) + fees_tao: Balance # RAO + fees_alpha: Balance # RAO + netuid: int + + def to_token_amounts( + self, current_subnet_price: Balance + ) -> tuple[Balance, Balance]: + """Convert a position to token amounts. + + Arguments: + current_subnet_price: current subnet price in Alpha. + + Returns: + tuple[int, int]: + Amount of Alpha in liquidity + Amount of TAO in liquidity + + Liquidity is a combination of TAO and Alpha depending on the price of the subnet at the moment. + """ + sqrt_price_low = math.sqrt(self.price_low) + sqrt_price_high = math.sqrt(self.price_high) + sqrt_current_subnet_price = math.sqrt(current_subnet_price) + + if sqrt_current_subnet_price < sqrt_price_low: + amount_alpha = self.liquidity * (1 / sqrt_price_low - 1 / sqrt_price_high) + amount_tao = 0 + elif sqrt_current_subnet_price > sqrt_price_high: + amount_alpha = 0 + amount_tao = self.liquidity * (sqrt_price_high - sqrt_price_low) + else: + amount_alpha = self.liquidity * ( + 1 / sqrt_current_subnet_price - 1 / sqrt_price_high + ) + amount_tao = self.liquidity * (sqrt_current_subnet_price - sqrt_price_low) + return Balance.from_rao(int(amount_alpha), self.netuid), Balance.from_rao( + int(amount_tao) + ) + + +def price_to_tick(price: float) -> int: + """Converts a float price to the nearest Uniswap V3 tick index.""" + if price <= 0: + raise ValueError(f"Price must be positive, got `{price}`.") + + tick = int(math.log(price) / math.log(PRICE_STEP)) + + if not (MIN_TICK <= tick <= MAX_TICK): + raise ValueError( + f"Resulting tick {tick} is out of allowed range ({MIN_TICK} to {MAX_TICK})" + ) + return tick + + +def tick_to_price(tick: int) -> float: + """Convert an integer Uniswap V3 tick index to float price.""" + if not (MIN_TICK <= tick <= MAX_TICK): + raise ValueError("Tick is out of allowed range") + return PRICE_STEP**tick + + +def get_fees( + current_tick: int, + tick: dict, + tick_index: int, + quote: bool, + global_fees_tao: float, + global_fees_alpha: float, + above: bool, +) -> float: + """Returns the liquidity fee.""" + tick_fee_key = "fees_out_tao" if quote else "fees_out_alpha" + tick_fee_value = fixed_to_float(tick.get(tick_fee_key)) + global_fee_value = global_fees_tao if quote else global_fees_alpha + + if above: + return ( + global_fee_value - tick_fee_value + if tick_index <= current_tick + else tick_fee_value + ) + return ( + tick_fee_value + if tick_index <= current_tick + else global_fee_value - tick_fee_value + ) + + +def get_fees_in_range( + quote: bool, + global_fees_tao: float, + global_fees_alpha: float, + fees_below_low: float, + fees_above_high: float, +) -> float: + """Returns the liquidity fee value in a range.""" + global_fees = global_fees_tao if quote else global_fees_alpha + return global_fees - fees_below_low - fees_above_high + + +# Calculate fees for a position +def calculate_fees( + position: dict[str, Any], + global_fees_tao: float, + global_fees_alpha: float, + tao_fees_below_low: float, + tao_fees_above_high: float, + alpha_fees_below_low: float, + alpha_fees_above_high: float, + netuid: int, +) -> tuple[Balance, Balance]: + fee_tao_agg = get_fees_in_range( + quote=True, + global_fees_tao=global_fees_tao, + global_fees_alpha=global_fees_alpha, + fees_below_low=tao_fees_below_low, + fees_above_high=tao_fees_above_high, + ) + + fee_alpha_agg = get_fees_in_range( + quote=False, + global_fees_tao=global_fees_tao, + global_fees_alpha=global_fees_alpha, + fees_below_low=alpha_fees_below_low, + fees_above_high=alpha_fees_above_high, + ) + + fee_tao = fee_tao_agg - fixed_to_float(position["fees_tao"]) + fee_alpha = fee_alpha_agg - fixed_to_float(position["fees_alpha"]) + liquidity_frac = position["liquidity"] + + fee_tao = liquidity_frac * fee_tao + fee_alpha = liquidity_frac * fee_alpha + + return Balance.from_rao(int(fee_tao)), Balance.from_rao(int(fee_alpha), netuid) diff --git a/bittensor/utils/logging.py b/bittensor/utils/logging.py deleted file mode 100644 index 6651d278f5..0000000000 --- a/bittensor/utils/logging.py +++ /dev/null @@ -1,367 +0,0 @@ -import bittensor -import torch -import numpy as np -import pandas as pd -from loguru import logger -from termcolor import colored -from typing import List -from bittensor import Session - -np.set_printoptions(precision=2, suppress=True, linewidth=500, sign=' ') -pd.set_option('display.max_rows', 5000) -pd.set_option('display.max_columns', 25) -pd.set_option('display.width', 1000) -pd.set_option('display.precision', 2) -pd.set_option('display.float_format', lambda x: '%.3f' % x) - -def log_all( session: Session, history: List[bittensor.synapse.SynapseOutput] ): - log_outputs(history) - log_batch_weights(session, history) - log_row_weights(session) - log_col_weights - log_incentive(session) - log_ranks(session) - log_request_sizes(session, history) - log_return_codes(session, history) - log_dendrite_success_times( session ) - - -def _calculate_request_sizes_sum(session: Session, history: List[bittensor.synapse.SynapseOutput]): - device = torch.device("cuda" if torch.cuda.is_available() else "cpu") - request_size_list = [] - request_sizes_sum = torch.zeros(session.metagraph.n).to(device) - for output in history: - request_sizes_sum += output.request_sizes - request_size_list.append(output.request_sizes) - request_sizes_sum = request_sizes_sum.tolist() - - return request_sizes_sum, request_size_list - -def log_dendrite_success_times(session: Session): - print ('Avg Success time: \n ') - remotes = session.dendrite.remotes - neurons = [remote.neuron for remote in remotes] - uids = session.metagraph.neurons_to_uids(neurons) - succees_time = [remote.stats.avg_success_time for remote in remotes] - df = pd.DataFrame([succees_time], columns=uids.tolist()) - pd.set_option('display.float_format', lambda x: '%.4f' % x) - df.rename_axis("[uid]", axis=1) - print (df) - print('\n') - -def log_ranks(session: Session): - print ('Ranks: \n ') - if session.metagraph.uids != None and session.metagraph.weights != None: - uids = session.metagraph.uids.tolist() - ranks = session.metagraph.ranks.tolist() - ranks, uids = zip(*sorted(zip(ranks, uids), reverse=True)) - df = pd.DataFrame([ranks], columns=uids) - df.rename_axis('[batch]').rename_axis("[uid]", axis=1) - max_val = df.max(numeric_only=True, axis=1) - min_val = df.min(numeric_only=True, axis=1) - total = df.sum(numeric_only=True, axis=1) - df.loc[:,'Min'] = min_val - df.loc[:,'Max'] = max_val - df.loc[:,'Total'] = total - print (df) - print('\n') - - -def log_incentive(session: Session): - print ('Incentive: \n ') - if session.metagraph.uids != None and session.metagraph.weights != None: - uids = session.metagraph.uids.tolist() - incentive = session.metagraph.incentive.tolist() - incentive, uids = zip(*sorted(zip(incentive, uids), reverse=True)) - df = pd.DataFrame([incentive], columns=uids) - df.rename_axis('[batch]').rename_axis("[uid]", axis=1) - max_val = df.max(numeric_only=True, axis=1) - min_val = df.min(numeric_only=True, axis=1) - total = df.sum(numeric_only=True, axis=1) - df.loc[:,'Min'] = min_val - df.loc[:,'Max'] = max_val - df.loc[:,'Total'] = total - print (df) - print('\n') - -def log_return_codes(session: Session, history: List[bittensor.synapse.SynapseOutput]): - print('Return Codes: \n ') - request_size_list = [] - request_sizes_sum = torch.zeros(session.metagraph.n) - for output in history: - request_sizes_sum += output.request_sizes - request_size_list.append(output.request_sizes) - request_sizes_sum = request_sizes_sum.tolist() - - return request_sizes_sum, request_size_list - -def log_return_codes(session: Session, history: List[bittensor.synapse.SynapseOutput]): - print('Return Codes: \n ') - request_sizes_sum, _ = _calculate_request_sizes_sum(session, history) - - rows = [] - for output in history: - _, retcodes = zip(*sorted(zip(request_sizes_sum, output.return_codes.tolist()), reverse=True)) - rows.append(retcodes) - _, uids = zip(*sorted(zip(request_sizes_sum, session.metagraph.uids.tolist()), reverse=True)) - pd.set_option('display.float_format', lambda x: '%.1f' % x) - df = pd.DataFrame(rows, columns=uids) - df.rename_axis('[batch]').rename_axis("[uid]", axis=1) - print (df) - print('\n') - -def log_request_sizes(session: Session, history: List[bittensor.synapse.SynapseOutput]): - print('Request Sizes: \n ') - request_sizes_sum, request_size_list = _calculate_request_sizes_sum(session, history) - - rows = [] - for rs in request_size_list: - _, rs = zip(*sorted(zip(request_sizes_sum, rs.tolist()), reverse=True)) - rows.append(rs) - _, uids = zip(*sorted(zip(request_sizes_sum, session.metagraph.uids.tolist()), reverse=True)) - pd.set_option('display.float_format', lambda x: '%.1f' % x) - df = pd.DataFrame(rows, columns=uids) - total_row = df.sum(numeric_only=True, axis=1) - total_col = df.sum(numeric_only=True, axis=0) - min_row = df.min(numeric_only=True, axis=1) - min_col = df.min(numeric_only=True, axis=0) - max_row = df.max(numeric_only=True, axis=1) - max_col = df.max(numeric_only=True, axis=0) - mean_row = df.mean(numeric_only=True, axis=1) - mean_col = df.mean(numeric_only=True, axis=0) - df.loc[:,'Min'] = min_row - df.loc[:,'Max'] = max_row - df.loc[:,'Mean'] = mean_row - df.loc[:,'Total'] = total_row - df.loc['Min'] = min_col - df.loc['Max'] = max_col - df.loc['Mean'] = mean_col - df.loc['Total'] = total_col - df.rename_axis('[batch]').rename_axis("[uid]", axis=1) - print (df) - print('\n') - -def log_row_weights(session: Session): - print ('Row Weights: \n ') - if session.metagraph.uids != None and session.metagraph.weights != None: - uids = session.metagraph.uids.tolist() - weights = session.metagraph.W[0,:].tolist() - weights, uids = zip(*sorted(zip(weights, uids), reverse=True)) - df = pd.DataFrame([weights], columns=uids) - df.rename_axis("[uid]", axis=1) - max_val = df.max(numeric_only=True, axis=1) - min_val = df.min(numeric_only=True, axis=1) - total = df.sum(numeric_only=True, axis=1) - df.loc[:,'Min'] = min_val - df.loc[:,'Max'] = max_val - df.loc[:,'Total'] = total - print (df) - print('\n') - -def log_col_weights(session: Session): - print ('Col Weights: \n ') - if session.metagraph.uids != None and session.metagraph.weights != None: - uids = session.metagraph.uids.tolist() - weights = session.metagraph.W[:,0].tolist() - weights, uids = zip(*sorted(zip(weights, uids), reverse=True)) - df = pd.DataFrame([weights], columns=uids) - df.rename_axis("[uid]", axis=1) - max_val = df.max(numeric_only=True, axis=1) - min_val = df.min(numeric_only=True, axis=1) - total = df.sum(numeric_only=True, axis=1) - df.loc[:,'Min'] = min_val - df.loc[:,'Max'] = max_val - df.loc[:,'Total'] = total - print (df) - print('\n') - -def log_batch_weights(session: Session, history: List[bittensor.synapse.SynapseOutput]): - print ('Batch Weights: \n ') - weights_sum = history[0].weights - for output in history[1:]: - weights_sum += torch.mean(output.weights, axis=0) - weights_sum = weights_sum.tolist() - - uids = session.metagraph.uids.tolist() - _, sorted_uids = zip(*sorted(zip(weights_sum, uids), reverse=True)) - - rows = [] - for output in history: - batch_weights = torch.mean(output.weights, axis=0).tolist() - _, sorted_batch_weights = zip(*sorted( zip(weights_sum, batch_weights), reverse=True)) - rows.append(sorted_batch_weights) - df = pd.DataFrame(rows, columns = sorted_uids) - min_row = df.min(numeric_only=True, axis=1) - min_col = df.min(numeric_only=True, axis=0) - max_row = df.max(numeric_only=True, axis=1) - max_col = df.max(numeric_only=True, axis=0) - mean_row = df.mean(numeric_only=True, axis=1) - mean_col = df.mean(numeric_only=True, axis=0) - df.loc[:,'Min'] = min_row - df.loc[:,'Max'] = max_row - df.loc[:,'Mean'] = mean_row - df.loc['Min'] = min_col - df.loc['Max'] = max_col - df.loc['Mean'] = mean_col - df.rename_axis('[batch]').rename_axis("[uid]", axis=1) - print (df) - print('\n') - -def log_outputs(history: List[bittensor.synapse.SynapseOutput]): - print ('Training Outputs: \n ') - cols = ['Batch Size', 'Total Loss', 'Local Loss', 'Remote Loss', 'Distillation Loss'] + list(history[0].metadata.keys()) - rows = [] - for output in history: - row = [] - - if output.local_hidden != None: - row.append(output.local_hidden.shape[0]) - else: - row.append('') - - if output.loss != None: - row.append(output.loss.item()) - else: - row.append('') - - if output.local_target_loss != None: - row.append(output.local_target_loss.item()) - else: - row.append('') - - if output.remote_target_loss != None: - row.append(output.remote_target_loss.item()) - else: - row.append('') - - if output.distillation_loss != None: - row.append(output.distillation_loss.item()) - else: - row.append('') - - for key in output.metadata.keys(): - row.append(output.metadata[key].item()) - - rows.append(row) - - pd.set_option('display.float_format', lambda x: '%.3f' % x) - df = pd.DataFrame(rows, columns=cols) - min_val = df.min(numeric_only=True, axis=0) - max_val = df.max(numeric_only=True, axis=0) - mean_val = df.mean(numeric_only=True, axis=0) - df.loc['Min'] = min_val - df.loc['Max'] = max_val - df.loc['Mean'] = mean_val - print (df) - print('\n') - - -def log_training_output_history(session, epoch, batch_idx, batch_size, total_examples, history): - - # Colorize outputs and log. - processed = ((batch_idx + 1) * batch_size) - progress = (100. * processed) / total_examples - total_str = colored('{}'.format(total_examples), 'red') - processed_str = colored('{}'.format(processed), 'green') - progress_str = colored('{:.2f}%'.format(progress), 'green') - logger.info('Epoch: {} [{}/{} ({})]', epoch, processed_str, total_str, progress_str) - print('\n') - - print ('Outputs: \n ') - try: - rows = [] - cols = ['Batch Size', 'Total Loss', 'Local Loss', 'Remote Loss', 'Distillation Loss'] + list(history[0].metadata.keys()) - for output in history: - row = [] - row.append(output.local_hidden.shape[0]) - row.append(output.loss.item()) - row.append(output.local_target_loss.item()) - row.append(output.remote_target_loss.item()) - row.append(output.distillation_loss.item()) - for key in output.metadata.keys(): - row.append(output.metadata[key].item()) - rows.append(row) - pd.set_option('display.float_format', lambda x: '%.3f' % x) - df = pd.DataFrame(rows, columns=cols) - min_val = df.min(numeric_only=True, axis=0) - max_val = df.max(numeric_only=True, axis=0) - mean_val = df.mean(numeric_only=True, axis=0) - df.loc['Min'] = min_val - df.loc['Max'] = max_val - df.loc['Mean'] = mean_val - print (df) - except Exception as e: - print ('Not set. Exception occured: {}'.format(e)) - print('\n') - - # Log chain weights - print ('Chain weights: \n ') - if session.metagraph.state.uids != None and session.metagraph.chain_weights != None: - uids = session.metagraph.state.uids.tolist() - weights = session.metagraph.chain_weights().tolist() - weights, uids = zip(*sorted(zip(weights, uids), reverse=True)) - df = pd.DataFrame([weights], columns=uids) - df.rename_axis('[batch]').rename_axis("[uid]", axis=1) - max_val = df.max(numeric_only=True, axis=1) - min_val = df.min(numeric_only=True, axis=1) - total = df.sum(numeric_only=True, axis=1) - df.loc[:,'Min'] = min_val - df.loc[:,'Max'] = max_val - df.loc[:,'Total'] = total - print (df) - print('\n') - - # Log batch weights - print ('Batch Weights: \n ') - if history[0].weights != None: - batch_weights_list = [] - for output in history: - batch_weights = torch.mean(output.weights, axis=0).tolist() - _, batch_weights = zip(*sorted( zip(weights, batch_weights), reverse=True)) - batch_weights_list.append(batch_weights) - df = pd.DataFrame(batch_weights_list, columns=uids) - min_row = df.min(numeric_only=True, axis=1) - min_col = df.min(numeric_only=True, axis=0) - max_row = df.max(numeric_only=True, axis=1) - max_col = df.max(numeric_only=True, axis=0) - mean_row = df.mean(numeric_only=True, axis=1) - mean_col = df.mean(numeric_only=True, axis=0) - df.loc[:,'Min'] = min_row - df.loc[:,'Max'] = max_row - df.loc[:,'Mean'] = mean_row - df.loc['Min'] = min_col - df.loc['Max'] = max_col - df.loc['Mean'] = mean_col - df.rename_axis('[batch]').rename_axis("[uid]", axis=1) - print (df) - print('\n') - - # Log return ops. - print('Requests: \n ') - if history[0].request_sizes != None: - sizes = [] - for output in history: - batch_sizes = output.request_sizes.tolist() - _, batch_sizes = zip(*sorted(zip(weights, batch_sizes), reverse=True)) - sizes.append(batch_sizes) - pd.set_option('display.float_format', lambda x: '%.1f' % x) - df = pd.DataFrame(sizes, columns=uids) - total_row = df.sum(numeric_only=True, axis=1) - total_col = df.sum(numeric_only=True, axis=0) - min_row = df.min(numeric_only=True, axis=1) - min_col = df.min(numeric_only=True, axis=0) - max_row = df.max(numeric_only=True, axis=1) - max_col = df.max(numeric_only=True, axis=0) - mean_row = df.mean(numeric_only=True, axis=1) - mean_col = df.mean(numeric_only=True, axis=1) - df.loc[:,'Min'] = min_row - df.loc[:,'Max'] = max_row - df.loc[:,'Mean'] = mean_row - df.loc[:,'Total'] = total_row - df.loc['Min'] = min_col - df.loc['Max'] = max_col - df.loc['Mean'] = mean_col - df.loc['Total'] = total_col - df.rename_axis('[batch]').rename_axis("[uid]", axis=1) - print (df) - print('\n') \ No newline at end of file diff --git a/bittensor/utils/mock/__init__.py b/bittensor/utils/mock/__init__.py new file mode 100644 index 0000000000..04893c78a3 --- /dev/null +++ b/bittensor/utils/mock/__init__.py @@ -0,0 +1 @@ +from .subtensor_mock import MockSubtensor diff --git a/bittensor/utils/mock/subtensor_mock.py b/bittensor/utils/mock/subtensor_mock.py new file mode 100644 index 0000000000..becbd90595 --- /dev/null +++ b/bittensor/utils/mock/subtensor_mock.py @@ -0,0 +1,1075 @@ +from collections.abc import Mapping +from dataclasses import dataclass +from hashlib import sha256 +from random import randint +from types import SimpleNamespace +from typing import Any, Optional, Union, TypedDict +from unittest.mock import MagicMock, patch + +from async_substrate_interface import SubstrateInterface +from bittensor_wallet import Wallet + +import bittensor.core.subtensor as subtensor_module +from bittensor.core.chain_data import ( + NeuronInfo, + NeuronInfoLite, + PrometheusInfo, + AxonInfo, +) +from bittensor.core.errors import ChainQueryError +from bittensor.core.subtensor import Subtensor +from bittensor.core.types import AxonServeCallParams, PrometheusServeCallParams +from bittensor.utils import RAOPERTAO, u16_normalized_float +from bittensor.utils.balance import Balance + +# Mock Testing Constant +__GLOBAL_MOCK_STATE__ = {} + + +BlockNumber = int + + +class InfoDict(Mapping): + @classmethod + def default(cls): + raise NotImplementedError + + def __getitem__(self, key): + return getattr(self, key) + + def __setitem__(self, key, value): + return setattr(self, key, value) + + def __iter__(self): + return iter(self.__dict__) + + def __len__(self): + return len(self.__dict__) + + +@dataclass +class AxonInfoDict(InfoDict): + block: int + version: int + ip: int # integer representation of ip address + port: int + ip_type: int + protocol: int + placeholder1: int # placeholder for future use + placeholder2: int + + @classmethod + def default(cls): + return cls( + block=0, + version=0, + ip=0, + port=0, + ip_type=0, + protocol=0, + placeholder1=0, + placeholder2=0, + ) + + +@dataclass +class PrometheusInfoDict(InfoDict): + block: int + version: int + ip: int # integer representation of ip address + port: int + ip_type: int + + @classmethod + def default(cls): + return cls(block=0, version=0, ip=0, port=0, ip_type=0) + + +@dataclass +class MockSubtensorValue: + value: Optional[Any] + + +class MockMapResult: + records: Optional[list[tuple[MockSubtensorValue, MockSubtensorValue]]] + + def __init__( + self, + records: Optional[ + list[tuple[Union[Any, MockSubtensorValue], Union[Any, MockSubtensorValue]]] + ] = None, + ): + _records = [ + ( + ( + MockSubtensorValue(value=record[0]), + MockSubtensorValue(value=record[1]), + ) + # Make sure record is a tuple of MockSubtensorValue (dict with value attr) + if not ( + isinstance(record, tuple) + and all( + isinstance(item, dict) and hasattr(item, "value") + for item in record + ) + ) + else record + ) + for record in records + ] + + self.records = _records + + def __iter__(self): + return iter(self.records) + + +class MockSystemState(TypedDict): + Account: dict[str, dict[int, int]] # address -> block -> balance + + +class MockSubtensorState(TypedDict): + Rho: dict[int, dict[BlockNumber, int]] # netuid -> block -> rho + Kappa: dict[int, dict[BlockNumber, int]] # netuid -> block -> kappa + Difficulty: dict[int, dict[BlockNumber, int]] # netuid -> block -> difficulty + ImmunityPeriod: dict[ + int, dict[BlockNumber, int] + ] # netuid -> block -> immunity_period + ValidatorBatchSize: dict[ + int, dict[BlockNumber, int] + ] # netuid -> block -> validator_batch_size + Active: dict[int, dict[BlockNumber, bool]] # (netuid, uid), block -> active + Stake: dict[str, dict[str, dict[int, int]]] # (hotkey, coldkey) -> block -> stake + + Delegates: dict[str, dict[int, float]] # address -> block -> delegate_take + + NetworksAdded: dict[int, dict[BlockNumber, bool]] # netuid -> block -> added + + +class MockChainState(TypedDict): + System: MockSystemState + SubtensorModule: MockSubtensorState + + +class ReusableCoroutine: + def __init__(self, coroutine): + self.coroutine = coroutine + + def __await__(self): + return self.reset().__await__() + + def reset(self): + return self.coroutine() + + +async def _async_block(): + return 1 + + +class MockSubtensor(Subtensor): + """ + A Mock Subtensor class for running tests. + This should mock only methods that make queries to the chain. + e.g. We mock `Subtensor.query_subtensor` instead of all query methods. + + This class will also store a local (mock) state of the chain. + """ + + chain_state: MockChainState + block_number: int + + @classmethod + def reset(cls) -> None: + __GLOBAL_MOCK_STATE__.clear() + + _ = cls() + + def setup(self) -> None: + if not hasattr(self, "chain_state") or getattr(self, "chain_state") is None: + self.chain_state = { + "System": {"Account": {}}, + "Balances": {"ExistentialDeposit": {0: 500}}, + "SubtensorModule": { + "NetworksAdded": {}, + "Rho": {}, + "Kappa": {}, + "Difficulty": {}, + "ImmunityPeriod": {}, + "ValidatorBatchSize": {}, + "ValidatorSequenceLength": {}, + "ValidatorEpochsPerReset": {}, + "ValidatorEpochLength": {}, + "MaxAllowedValidators": {}, + "MinAllowedWeights": {}, + "MaxWeightLimit": {}, + "SynergyScalingLawPower": {}, + "ScalingLawPower": {}, + "SubnetworkN": {}, + "MaxAllowedUids": {}, + "NetworkModality": {}, + "BlocksSinceLastStep": {}, + "Tempo": {}, + "NetworkConnect": {}, + "EmissionValues": {}, + "Burn": {}, + "Active": {}, + "Uids": {}, + "Keys": {}, + "Owner": {}, + "IsNetworkMember": {}, + "LastUpdate": {}, + "Rank": {}, + "Emission": {}, + "Incentive": {}, + "Consensus": {}, + "Trust": {}, + "ValidatorTrust": {}, + "Dividends": {}, + "PruningScores": {}, + "ValidatorPermit": {}, + "Weights": {}, + "Bonds": {}, + "Stake": {}, + "TotalStake": {0: 0}, + "TotalIssuance": {0: 0}, + "TotalHotkeyStake": {}, + "TotalColdkeyStake": {}, + "TxRateLimit": {0: 0}, # No limit + "Delegates": {}, + "Axons": {}, + "Prometheus": {}, + "SubnetOwner": {}, + "Commits": {}, + "AdjustmentAlpha": {}, + "BondsMovingAverage": {}, + }, + } + + self.block_number = 0 + + self.network = "mock" + self.chain_endpoint = "ws://mock_endpoint.bt" + self.substrate = MagicMock(autospec=SubstrateInterface) + + def __init__(self, *args, **kwargs) -> None: + mock_substrate_interface = MagicMock(autospec=SubstrateInterface) + with patch.object( + subtensor_module, + "SubstrateInterface", + return_value=mock_substrate_interface, + ): + super().__init__() + self.__dict__ = __GLOBAL_MOCK_STATE__ + + if not hasattr(self, "chain_state") or getattr(self, "chain_state") is None: + self.setup() + + def get_block_hash(self, block: Optional[int] = None) -> str: + return "0x" + sha256(str(block).encode()).hexdigest()[:64] + + def create_subnet(self, netuid: int) -> None: + subtensor_state = self.chain_state["SubtensorModule"] + if netuid not in subtensor_state["NetworksAdded"]: + # Per Subnet + subtensor_state["Rho"][netuid] = {} + subtensor_state["Rho"][netuid][0] = 10 + subtensor_state["Kappa"][netuid] = {} + subtensor_state["Kappa"][netuid][0] = 32_767 + subtensor_state["Difficulty"][netuid] = {} + subtensor_state["Difficulty"][netuid][0] = 10_000_000 + subtensor_state["ImmunityPeriod"][netuid] = {} + subtensor_state["ImmunityPeriod"][netuid][0] = 4096 + subtensor_state["ValidatorBatchSize"][netuid] = {} + subtensor_state["ValidatorBatchSize"][netuid][0] = 32 + subtensor_state["ValidatorSequenceLength"][netuid] = {} + subtensor_state["ValidatorSequenceLength"][netuid][0] = 256 + subtensor_state["ValidatorEpochsPerReset"][netuid] = {} + subtensor_state["ValidatorEpochsPerReset"][netuid][0] = 60 + subtensor_state["ValidatorEpochLength"][netuid] = {} + subtensor_state["ValidatorEpochLength"][netuid][0] = 100 + subtensor_state["MaxAllowedValidators"][netuid] = {} + subtensor_state["MaxAllowedValidators"][netuid][0] = 128 + subtensor_state["MinAllowedWeights"][netuid] = {} + subtensor_state["MinAllowedWeights"][netuid][0] = 1024 + subtensor_state["MaxWeightLimit"][netuid] = {} + subtensor_state["MaxWeightLimit"][netuid][0] = 1_000 + subtensor_state["SynergyScalingLawPower"][netuid] = {} + subtensor_state["SynergyScalingLawPower"][netuid][0] = 50 + subtensor_state["ScalingLawPower"][netuid] = {} + subtensor_state["ScalingLawPower"][netuid][0] = 50 + subtensor_state["SubnetworkN"][netuid] = {} + subtensor_state["SubnetworkN"][netuid][0] = 0 + subtensor_state["MaxAllowedUids"][netuid] = {} + subtensor_state["MaxAllowedUids"][netuid][0] = 4096 + subtensor_state["NetworkModality"][netuid] = {} + subtensor_state["NetworkModality"][netuid][0] = 0 + subtensor_state["BlocksSinceLastStep"][netuid] = {} + subtensor_state["BlocksSinceLastStep"][netuid][0] = 0 + subtensor_state["Tempo"][netuid] = {} + subtensor_state["Tempo"][netuid][0] = 99 + + # subtensor_state['NetworkConnect'][netuid] = {} + # subtensor_state['NetworkConnect'][netuid][0] = {} + subtensor_state["EmissionValues"][netuid] = {} + subtensor_state["EmissionValues"][netuid][0] = 0 + subtensor_state["Burn"][netuid] = {} + subtensor_state["Burn"][netuid][0] = 0 + subtensor_state["Commits"][netuid] = {} + + # Per-UID/Hotkey + + subtensor_state["Uids"][netuid] = {} + subtensor_state["Keys"][netuid] = {} + subtensor_state["Owner"][netuid] = {} + + subtensor_state["LastUpdate"][netuid] = {} + subtensor_state["Active"][netuid] = {} + subtensor_state["Rank"][netuid] = {} + subtensor_state["Emission"][netuid] = {} + subtensor_state["Incentive"][netuid] = {} + subtensor_state["Consensus"][netuid] = {} + subtensor_state["Trust"][netuid] = {} + subtensor_state["ValidatorTrust"][netuid] = {} + subtensor_state["Dividends"][netuid] = {} + subtensor_state["PruningScores"][netuid] = {} + subtensor_state["PruningScores"][netuid][0] = {} + subtensor_state["ValidatorPermit"][netuid] = {} + + subtensor_state["Weights"][netuid] = {} + subtensor_state["Bonds"][netuid] = {} + + subtensor_state["Axons"][netuid] = {} + subtensor_state["Prometheus"][netuid] = {} + + subtensor_state["NetworksAdded"][netuid] = {} + subtensor_state["NetworksAdded"][netuid][0] = True + + subtensor_state["AdjustmentAlpha"][netuid] = {} + subtensor_state["AdjustmentAlpha"][netuid][0] = 1000 + + subtensor_state["BondsMovingAverage"][netuid] = {} + subtensor_state["BondsMovingAverage"][netuid][0] = 1000 + else: + raise Exception("Subnet already exists") + + def set_difficulty(self, netuid: int, difficulty: int) -> None: + subtensor_state = self.chain_state["SubtensorModule"] + if netuid not in subtensor_state["NetworksAdded"]: + raise Exception("Subnet does not exist") + + subtensor_state["Difficulty"][netuid][self.block_number] = difficulty + + def _register_neuron(self, netuid: int, hotkey: str, coldkey: str) -> int: + subtensor_state = self.chain_state["SubtensorModule"] + if netuid not in subtensor_state["NetworksAdded"]: + raise Exception("Subnet does not exist") + + subnetwork_n = self._get_most_recent_storage( + subtensor_state["SubnetworkN"][netuid] + ) + + if subnetwork_n > 0 and any( + self._get_most_recent_storage(subtensor_state["Keys"][netuid][uid]) + == hotkey + for uid in range(subnetwork_n) + ): + # already_registered + raise Exception("Hotkey already registered") + else: + # Not found + if subnetwork_n >= self._get_most_recent_storage( + subtensor_state["MaxAllowedUids"][netuid] + ): + # Subnet full, replace neuron randomly + uid = randint(0, subnetwork_n - 1) + else: + # Subnet not full, add new neuron + # Append as next uid and increment subnetwork_n + uid = subnetwork_n + subtensor_state["SubnetworkN"][netuid][self.block_number] = ( + subnetwork_n + 1 + ) + + subtensor_state["Stake"][hotkey] = {} + subtensor_state["Stake"][hotkey][coldkey] = {} + subtensor_state["Stake"][hotkey][coldkey][self.block_number] = 0 + + subtensor_state["Uids"][netuid][hotkey] = {} + subtensor_state["Uids"][netuid][hotkey][self.block_number] = uid + + subtensor_state["Keys"][netuid][uid] = {} + subtensor_state["Keys"][netuid][uid][self.block_number] = hotkey + + subtensor_state["Owner"][hotkey] = {} + subtensor_state["Owner"][hotkey][self.block_number] = coldkey + + subtensor_state["Active"][netuid][uid] = {} + subtensor_state["Active"][netuid][uid][self.block_number] = True + + subtensor_state["LastUpdate"][netuid][uid] = {} + subtensor_state["LastUpdate"][netuid][uid][self.block_number] = ( + self.block_number + ) + + subtensor_state["Rank"][netuid][uid] = {} + subtensor_state["Rank"][netuid][uid][self.block_number] = 0.0 + + subtensor_state["Emission"][netuid][uid] = {} + subtensor_state["Emission"][netuid][uid][self.block_number] = 0.0 + + subtensor_state["Incentive"][netuid][uid] = {} + subtensor_state["Incentive"][netuid][uid][self.block_number] = 0.0 + + subtensor_state["Consensus"][netuid][uid] = {} + subtensor_state["Consensus"][netuid][uid][self.block_number] = 0.0 + + subtensor_state["Trust"][netuid][uid] = {} + subtensor_state["Trust"][netuid][uid][self.block_number] = 0.0 + + subtensor_state["ValidatorTrust"][netuid][uid] = {} + subtensor_state["ValidatorTrust"][netuid][uid][self.block_number] = 0.0 + + subtensor_state["Dividends"][netuid][uid] = {} + subtensor_state["Dividends"][netuid][uid][self.block_number] = 0.0 + + subtensor_state["PruningScores"][netuid][uid] = {} + subtensor_state["PruningScores"][netuid][uid][self.block_number] = 0.0 + + subtensor_state["ValidatorPermit"][netuid][uid] = {} + subtensor_state["ValidatorPermit"][netuid][uid][self.block_number] = False + + subtensor_state["Weights"][netuid][uid] = {} + subtensor_state["Weights"][netuid][uid][self.block_number] = [] + + subtensor_state["Bonds"][netuid][uid] = {} + subtensor_state["Bonds"][netuid][uid][self.block_number] = [] + + subtensor_state["Axons"][netuid][hotkey] = {} + subtensor_state["Axons"][netuid][hotkey][self.block_number] = {} + + subtensor_state["Prometheus"][netuid][hotkey] = {} + subtensor_state["Prometheus"][netuid][hotkey][self.block_number] = {} + + if hotkey not in subtensor_state["IsNetworkMember"]: + subtensor_state["IsNetworkMember"][hotkey] = {} + subtensor_state["IsNetworkMember"][hotkey][netuid] = {} + subtensor_state["IsNetworkMember"][hotkey][netuid][self.block_number] = True + + return uid + + @staticmethod + def _convert_to_balance(balance: Union["Balance", float, int]) -> "Balance": + if isinstance(balance, float): + balance = Balance.from_tao(balance) + + if isinstance(balance, int): + balance = Balance.from_rao(balance) + + return balance + + def force_register_neuron( + self, + netuid: int, + hotkey: str, + coldkey: str, + stake: Union["Balance", float, int] = Balance(0), + balance: Union["Balance", float, int] = Balance(0), + ) -> int: + """ + Force register a neuron on the mock chain, returning the UID. + """ + stake = self._convert_to_balance(stake) + balance = self._convert_to_balance(balance) + + subtensor_state = self.chain_state["SubtensorModule"] + if netuid not in subtensor_state["NetworksAdded"]: + raise Exception("Subnet does not exist") + + uid = self._register_neuron(netuid=netuid, hotkey=hotkey, coldkey=coldkey) + + subtensor_state["TotalStake"][self.block_number] = ( + self._get_most_recent_storage(subtensor_state["TotalStake"]) + stake.rao + ) + subtensor_state["Stake"][hotkey][coldkey][self.block_number] = stake.rao + + if balance.rao > 0: + self.force_set_balance(coldkey, balance) + self.force_set_balance(coldkey, balance) + + return uid + + def force_set_balance( + self, ss58_address: str, balance: Union["Balance", float, int] = Balance(0) + ) -> tuple[bool, Optional[str]]: + """ + Returns: + tuple[bool, Optional[str]]: (success, err_msg) + """ + balance = self._convert_to_balance(balance) + + if ss58_address not in self.chain_state["System"]["Account"]: + self.chain_state["System"]["Account"][ss58_address] = { + "data": {"free": {0: 0}} + } + + old_balance = self.get_balance(ss58_address, self.block_number) + diff = balance.rao - old_balance.rao + + # Update total issuance + self.chain_state["SubtensorModule"]["TotalIssuance"][self.block_number] = ( + self._get_most_recent_storage( + self.chain_state["SubtensorModule"]["TotalIssuance"] + ) + + diff + ) + + self.chain_state["System"]["Account"][ss58_address] = { + "data": {"free": {self.block_number: balance.rao}} + } + + return True, None + + # Alias for force_set_balance + sudo_force_set_balance = force_set_balance + + def do_block_step(self) -> None: + self.block_number += 1 + + # Doesn't do epoch + subtensor_state = self.chain_state["SubtensorModule"] + for subnet in subtensor_state["NetworksAdded"]: + subtensor_state["BlocksSinceLastStep"][subnet][self.block_number] = ( + self._get_most_recent_storage( + subtensor_state["BlocksSinceLastStep"][subnet] + ) + + 1 + ) + + def _handle_type_default(self, name: str, params: list[object]) -> object: + defaults_mapping = { + "TotalStake": 0, + "TotalHotkeyStake": 0, + "TotalColdkeyStake": 0, + "Stake": 0, + } + + return defaults_mapping.get(name, None) + + def commit(self, wallet: "Wallet", netuid: int, data: str) -> None: + uid = self.get_uid_for_hotkey_on_subnet( + hotkey_ss58=wallet.hotkey.ss58_address, + netuid=netuid, + ) + if uid is None: + raise Exception("Neuron not found") + subtensor_state = self.chain_state["SubtensorModule"] + subtensor_state["Commits"][netuid].setdefault(self.block_number, {})[uid] = data + + def get_commitment(self, netuid: int, uid: int, block: Optional[int] = None) -> str: + if block and self.block_number < block: + raise Exception("Cannot query block in the future") + block = block or self.block_number + + subtensor_state = self.chain_state["SubtensorModule"] + return subtensor_state["Commits"][netuid][block][uid] + + def query_subtensor( + self, + name: str, + block: Optional[int] = None, + params: Optional[list[object]] = None, + ) -> MockSubtensorValue: + if params is None: + params = [] + if block is not None: + if self.block_number < block: + raise Exception("Cannot query block in the future") + + else: + block = self.block_number + + state = self.chain_state["SubtensorModule"][name] + if state is not None: + # Use prefix + if len(params) > 0: + while state is not None and len(params) > 0: + state = state.get(params.pop(0), None) + if state is None: + return SimpleNamespace( + value=self._handle_type_default(name, params) + ) + + # Use block + state_at_block = state.get(block, None) + while state_at_block is None and block > 0: + block -= 1 + state_at_block = state.get(block, None) + if state_at_block is not None: + return SimpleNamespace(value=state_at_block) + + return SimpleNamespace(value=self._handle_type_default(name, params)) + else: + return SimpleNamespace(value=self._handle_type_default(name, params)) + + def query_map_subtensor( + self, + name: str, + block: Optional[int] = None, + params: Optional[list[object]] = None, + ) -> Optional[MockMapResult]: + """ + Note: Double map requires one param + """ + if params is None: + params = [] + if block is not None: + if self.block_number < block: + raise Exception("Cannot query block in the future") + + else: + block = self.block_number + + state = self.chain_state["SubtensorModule"][name] + if state is not None: + # Use prefix + if len(params) > 0: + while state is not None and len(params) > 0: + state = state.get(params.pop(0), None) + if state is None: + return MockMapResult([]) + + # Check if single map or double map + if len(state.keys()) == 0: + return MockMapResult([]) + + inner = list(state.values())[0] + # Should have at least one key + if len(inner.keys()) == 0: + raise Exception("Invalid state") + + # Check if double map + if isinstance(list(inner.values())[0], dict): + # is double map + raise ChainQueryError("Double map requires one param") + + # Iterate over each key and add value to list, max at block + records = [] + for key in state: + result = self._get_most_recent_storage(state[key], block) + if result is None: + continue # Skip if no result for this key at `block` or earlier + + records.append((key, result)) + + return MockMapResult(records) + else: + return MockMapResult([]) + + def query_constant( + self, module_name: str, constant_name: str, block: Optional[int] = None + ) -> Optional[object]: + if block is not None: + if self.block_number < block: + raise Exception("Cannot query block in the future") + + else: + block = self.block_number + + state: Optional[dict] = self.chain_state.get(module_name, None) + if state is not None: + if constant_name in state: + state = state[constant_name] + else: + return None + + # Use block + state_at_block = self._get_most_recent_storage(state, block) + if state_at_block is not None: + return SimpleNamespace(value=state_at_block) + + return state_at_block["data"]["free"] # Can be None + else: + return None + + def get_current_block(self) -> int: + return self.block_number + + # ==== Balance RPC methods ==== + + def get_balance(self, address: str, block: int = None) -> "Balance": + if block is not None: + if self.block_number < block: + raise Exception("Cannot query block in the future") + + else: + block = self.block_number + + state = self.chain_state["System"]["Account"] + if state is not None: + if address in state: + state = state[address] + else: + return Balance(0) + + # Use block + balance_state = state["data"]["free"] + state_at_block = self._get_most_recent_storage( + balance_state, block + ) # Can be None + if state_at_block is not None: + bal_as_int = state_at_block + return Balance.from_rao(bal_as_int) + else: + return Balance(0) + else: + return Balance(0) + + # ==== Neuron RPC methods ==== + + def neuron_for_uid( + self, uid: int, netuid: int, block: Optional[int] = None + ) -> Optional[NeuronInfo]: + if uid is None: + return NeuronInfo.get_null_neuron() + + if block is not None: + if self.block_number < block: + raise Exception("Cannot query block in the future") + + else: + block = self.block_number + + if netuid not in self.chain_state["SubtensorModule"]["NetworksAdded"]: + return None + + neuron_info = self._neuron_subnet_exists(uid, netuid, block) + if neuron_info is None: + return None + + else: + return neuron_info + + def neurons(self, netuid: int, block: Optional[int] = None) -> list[NeuronInfo]: + if netuid not in self.chain_state["SubtensorModule"]["NetworksAdded"]: + raise Exception("Subnet does not exist") + + neurons = [] + subnet_n = self._get_most_recent_storage( + self.chain_state["SubtensorModule"]["SubnetworkN"][netuid], block + ) + for uid in range(subnet_n): + neuron_info = self.neuron_for_uid(uid, netuid, block) + if neuron_info is not None: + neurons.append(neuron_info) + + return neurons + + @staticmethod + def _get_most_recent_storage( + storage: dict[BlockNumber, Any], block_number: Optional[int] = None + ) -> Any: + if block_number is None: + items = list(storage.items()) + items.sort(key=lambda x: x[0], reverse=True) + if len(items) == 0: + return None + + return items[0][1] + + else: + while block_number >= 0: + if block_number in storage: + return storage[block_number] + + block_number -= 1 + + return None + + def _get_axon_info( + self, netuid: int, hotkey: str, block: Optional[int] = None + ) -> AxonInfoDict: + # Axons [netuid][hotkey][block_number] + subtensor_state = self.chain_state["SubtensorModule"] + if netuid not in subtensor_state["Axons"]: + return AxonInfoDict.default() + + if hotkey not in subtensor_state["Axons"][netuid]: + return AxonInfoDict.default() + + result = self._get_most_recent_storage( + subtensor_state["Axons"][netuid][hotkey], block + ) + if not result: + return AxonInfoDict.default() + + return result + + def _get_prometheus_info( + self, netuid: int, hotkey: str, block: Optional[int] = None + ) -> PrometheusInfoDict: + subtensor_state = self.chain_state["SubtensorModule"] + if netuid not in subtensor_state["Prometheus"]: + return PrometheusInfoDict.default() + + if hotkey not in subtensor_state["Prometheus"][netuid]: + return PrometheusInfoDict.default() + + result = self._get_most_recent_storage( + subtensor_state["Prometheus"][netuid][hotkey], block + ) + if not result: + return PrometheusInfoDict.default() + + return result + + def _neuron_subnet_exists( + self, uid: int, netuid: int, block: Optional[int] = None + ) -> Optional[NeuronInfo]: + subtensor_state = self.chain_state["SubtensorModule"] + if netuid not in subtensor_state["NetworksAdded"]: + return None + + if self._get_most_recent_storage(subtensor_state["SubnetworkN"][netuid]) <= uid: + return None + + hotkey = self._get_most_recent_storage(subtensor_state["Keys"][netuid][uid]) + if hotkey is None: + return None + + axon_info_ = self._get_axon_info(netuid, hotkey, block) + + prometheus_info = self._get_prometheus_info(netuid, hotkey, block) + + coldkey = self._get_most_recent_storage(subtensor_state["Owner"][hotkey], block) + active = self._get_most_recent_storage( + subtensor_state["Active"][netuid][uid], block + ) + rank = self._get_most_recent_storage( + subtensor_state["Rank"][netuid][uid], block + ) + emission = self._get_most_recent_storage( + subtensor_state["Emission"][netuid][uid], block + ) + incentive = self._get_most_recent_storage( + subtensor_state["Incentive"][netuid][uid], block + ) + consensus = self._get_most_recent_storage( + subtensor_state["Consensus"][netuid][uid], block + ) + trust = self._get_most_recent_storage( + subtensor_state["Trust"][netuid][uid], block + ) + validator_trust = self._get_most_recent_storage( + subtensor_state["ValidatorTrust"][netuid][uid], block + ) + 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 + ) + validator_permit = self._get_most_recent_storage( + subtensor_state["ValidatorPermit"][netuid][uid], block + ) + + weights = self._get_most_recent_storage( + subtensor_state["Weights"][netuid][uid], block + ) + bonds = self._get_most_recent_storage( + subtensor_state["Bonds"][netuid][uid], block + ) + + stake_dict = { + coldkey: Balance.from_rao( + self._get_most_recent_storage( + subtensor_state["Stake"][hotkey][coldkey], block + ) + ) + for coldkey in subtensor_state["Stake"][hotkey] + } + + stake = sum(stake_dict.values()) + + weights = [[int(weight[0]), int(weight[1])] for weight in weights] + bonds = [[int(bond[0]), int(bond[1])] for bond in bonds] + rank = u16_normalized_float(rank) + emission = emission / RAOPERTAO + incentive = u16_normalized_float(incentive) + consensus = u16_normalized_float(consensus) + trust = u16_normalized_float(trust) + validator_trust = u16_normalized_float(validator_trust) + dividends = u16_normalized_float(dividends) + prometheus_info = PrometheusInfo.from_dict(prometheus_info) + axon_info_ = AxonInfo.from_neuron_info( + {"hotkey": hotkey, "coldkey": coldkey, "axon_info": axon_info_} + ) + + neuron_info = NeuronInfo( + hotkey=hotkey, + coldkey=coldkey, + uid=uid, + netuid=netuid, + active=active, + rank=rank, + emission=emission, + incentive=incentive, + consensus=consensus, + trust=trust, + validator_trust=validator_trust, + dividends=dividends, + pruning_score=pruning_score, + last_update=last_update, + validator_permit=validator_permit, + stake=stake, + stake_dict=stake_dict, + total_stake=stake, + prometheus_info=prometheus_info, + axon_info=axon_info_, + weights=weights, + bonds=bonds, + is_null=False, + ) + + return neuron_info + + def neurons_lite( + self, netuid: int, block: Optional[int] = None + ) -> list[NeuronInfoLite]: + if netuid not in self.chain_state["SubtensorModule"]["NetworksAdded"]: + raise Exception("Subnet does not exist") + + neurons = [] + subnet_n = self._get_most_recent_storage( + self.chain_state["SubtensorModule"]["SubnetworkN"][netuid] + ) + for uid in range(subnet_n): + neuron_info = self.neuron_for_uid_lite(uid, netuid, block) + if neuron_info is not None: + neurons.append(neuron_info) + + return neurons + + def neuron_for_uid_lite( + self, uid: int, netuid: int, block: Optional[int] = None + ) -> Optional[NeuronInfoLite]: + if uid is None: + return NeuronInfoLite.get_null_neuron() + + if block is not None: + if self.block_number < block: + raise Exception("Cannot query block in the future") + + else: + block = self.block_number + + if netuid not in self.chain_state["SubtensorModule"]["NetworksAdded"]: + return None + + neuron_info = self._neuron_subnet_exists(uid, netuid, block) + if neuron_info is None: + # TODO Why does this return None here but a null neuron earlier? + return None + + else: + return NeuronInfoLite( + hotkey=neuron_info.hotkey, + coldkey=neuron_info.coldkey, + uid=neuron_info.uid, + netuid=neuron_info.netuid, + active=neuron_info.active, + stake=neuron_info.stake, + stake_dict=neuron_info.stake_dict, + total_stake=neuron_info.total_stake, + rank=neuron_info.rank, + emission=neuron_info.emission, + incentive=neuron_info.incentive, + consensus=neuron_info.consensus, + trust=neuron_info.trust, + validator_trust=neuron_info.validator_trust, + dividends=neuron_info.dividends, + last_update=neuron_info.last_update, + validator_permit=neuron_info.validator_permit, + prometheus_info=neuron_info.prometheus_info, + axon_info=neuron_info.axon_info, + pruning_score=neuron_info.pruning_score, + is_null=neuron_info.is_null, + ) + + def get_transfer_fee( + self, wallet: "Wallet", dest: str, value: Union["Balance", float, int] + ) -> "Balance": + return Balance(700) + + def do_transfer( + self, + wallet: "Wallet", + dest: str, + transfer_balance: "Balance", + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, + ) -> tuple[bool, Optional[str], Optional[str]]: + bal = self.get_balance(wallet.coldkeypub.ss58_address) + dest_bal = self.get_balance(dest) + transfer_fee = self.get_transfer_fee(wallet, dest, transfer_balance) + + existential_deposit = self.get_existential_deposit() + + if bal < transfer_balance + existential_deposit + transfer_fee: + raise Exception("Insufficient balance") + + # Remove from the free balance + self.chain_state["System"]["Account"][wallet.coldkeypub.ss58_address]["data"][ + "free" + ][self.block_number] = (bal - transfer_balance - transfer_fee).rao + + # Add to the free balance + if dest not in self.chain_state["System"]["Account"]: + self.chain_state["System"]["Account"][dest] = {"data": {"free": {}}} + + self.chain_state["System"]["Account"][dest]["data"]["free"][ + self.block_number + ] = (dest_bal + transfer_balance).rao + + return True, None, None + + @staticmethod + def min_required_stake(): + """ + As the minimum required stake may change, this method allows us to dynamically + update the amount in the mock without updating the tests + """ + # valid minimum threshold as of 2024/05/01 + return 100_000_000 # RAO + + def do_serve_prometheus( + self, + wallet: "Wallet", + call_params: "PrometheusServeCallParams", + wait_for_inclusion: bool = False, + wait_for_finalization: bool = True, + ) -> tuple[bool, Optional[str]]: + return True, None + + def do_set_weights( + self, + wallet: "Wallet", + netuid: int, + uids: int, + vals: list[int], + version_key: int, + wait_for_inclusion: bool = False, + wait_for_finalization: bool = True, + ) -> tuple[bool, Optional[str]]: + return True, None + + def do_serve_axon( + self, + wallet: "Wallet", + call_params: "AxonServeCallParams", + wait_for_inclusion: bool = False, + wait_for_finalization: bool = True, + ) -> tuple[bool, Optional[str]]: + return True, None diff --git a/bittensor/utils/networking.py b/bittensor/utils/networking.py new file mode 100644 index 0000000000..e01012d8b7 --- /dev/null +++ b/bittensor/utils/networking.py @@ -0,0 +1,142 @@ +"""Utils for handling local network with ip and ports.""" + +import os +from typing import Optional +from urllib import request as urllib_request + +import netaddr +import requests +from async_substrate_interface.utils import json + + +class ExternalIPNotFound(Exception): + """Raised if we cannot attain your external ip from CURL/URLLIB/IPIFY/AWS""" + + +def int_to_ip(int_val: int) -> str: + """Maps an integer to a unique ip-string + + Arguments: + int_val (int): The integer representation of an ip. Must be in the range (0, 3.4028237e+38). + + Returns: + str_val (str): The string representation of an ip. Of form *.*.*.* for ipv4 or *::*:*:*:* for ipv6 + """ + return str(netaddr.IPAddress(int_val)) + + +def ip_to_int(str_val: str) -> int: + """Maps an ip-string to a unique integer. + + Arguments: + str_val (str): The string representation of an ip. Of form *.*.*.* for ipv4 or *::*:*:*:* for ipv6 + + Returns: + int_val (int): The integer representation of an ip. Must be in the range (0, 3.4028237e+38). + """ + return int(netaddr.IPAddress(str_val)) + + +def ip_version(str_val: str) -> int: + """Returns the ip version (IPV4 or IPV6). + + Arguments: + str_val (str): The string representation of an ip. Of form *.*.*.* for ipv4 or *::*:*:*:* for ipv6 + + Returns: + int_val (int): The ip version (Either 4 or 6 for IPv4/IPv6) + """ + return int(netaddr.IPAddress(str_val).version) + + +def ip__str__(ip_type: int, ip_str: str, port: int): + """Return a formatted ip string""" + return "/ipv%i/%s:%i" % (ip_type, ip_str, port) + + +def get_external_ip() -> str: + """Checks CURL/URLLIB/IPIFY/AWS for your external ip. + + Returns: + external_ip (str): Your routers external facing ip as a string. + + Raises: + ExternalIPNotFound(Exception): Raised if all external ip attempts fail. + """ + # --- Try AWS + try: + external_ip = requests.get("https://checkip.amazonaws.com").text.strip() + assert isinstance(ip_to_int(external_ip), int) + return str(external_ip) + except ExternalIPNotFound: + pass + + # --- Try ipconfig. + try: + process = os.popen("curl -s ifconfig.me") + external_ip = process.readline() + process.close() + assert isinstance(ip_to_int(external_ip), int) + return str(external_ip) + except ExternalIPNotFound: + pass + + # --- Try ipinfo. + try: + process = os.popen("curl -s https://ipinfo.io") + external_ip = json.loads(process.read())["ip"] + process.close() + assert isinstance(ip_to_int(external_ip), int) + return str(external_ip) + except ExternalIPNotFound: + pass + + # --- Try myip.dnsomatic + try: + process = os.popen("curl -s myip.dnsomatic.com") + external_ip = process.readline() + process.close() + assert isinstance(ip_to_int(external_ip), int) + return str(external_ip) + except ExternalIPNotFound: + pass + + # --- Try urllib ipv6 + try: + external_ip = urllib_request.urlopen("https://ident.me").read().decode("utf8") + assert isinstance(ip_to_int(external_ip), int) + return str(external_ip) + except ExternalIPNotFound: + pass + + # --- Try Wikipedia + try: + external_ip = requests.get("https://www.wikipedia.org").headers["X-Client-IP"] + assert isinstance(ip_to_int(external_ip), int) + return str(external_ip) + except ExternalIPNotFound: + pass + + raise ExternalIPNotFound + + +def get_formatted_ws_endpoint_url(endpoint_url: Optional[str]) -> Optional[str]: + """ + Returns a formatted websocket endpoint url. + + Arguments: + endpoint_url (Optional[str]): The endpoint url to format. + + Returns: + formatted_endpoint_url (Optional[str]): The formatted endpoint url. In the form of ws:// or + wss:// + + Note: The port (or lack thereof) is left unchanged. + """ + if endpoint_url is None: + return None + + if endpoint_url[0:6] != "wss://" and endpoint_url[0:5] != "ws://": + endpoint_url = f"ws://{endpoint_url}" + + return endpoint_url diff --git a/bittensor/utils/registration/__init__.py b/bittensor/utils/registration/__init__.py new file mode 100644 index 0000000000..ea527e4dc3 --- /dev/null +++ b/bittensor/utils/registration/__init__.py @@ -0,0 +1,21 @@ +from bittensor.utils.registration.pow import ( + create_pow, + 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, + legacy_torch_api_compat, + log_no_torch_error, + torch, + use_torch, + LazyLoadedTorch, + POWSolution, + create_pow_async, +] diff --git a/bittensor/utils/registration/async_pow.py b/bittensor/utils/registration/async_pow.py new file mode 100644 index 0000000000..fb8b9bef7d --- /dev/null +++ b/bittensor/utils/registration/async_pow.py @@ -0,0 +1,540 @@ +"""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.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. + + Args: + subtensor (bittensor.core.async_subtensor.AsyncSubtensor): The subtensor object to use to get the block number, + difficulty, and block hash. + netuid (int): 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. + + Args: + subtensor (AsyncSubtensor): The subtensor instance interface. + netuid (int): The network UID for the blockchain. + old_block_number (int): The previously known block number. + hotkey_bytes (bytes): The bytes representation of the hotkey. + curr_diff (Array): The current difficulty level. + curr_block (Array): The current block information. + curr_block_num (Value): The current block number. + update_curr_block_ (Callable): Function to update current block information. + check_block (Lock): Lock object for synchronizing block checking. + solvers (list[Solver]): List of solvers to notify of new blocks. + curr_stats (RegistrationStatistics): 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 + + Args: + subtensor (bittensor.core.async_subtensor.AsyncSubtensor): The subtensor object to use to get the block number, + difficulty, and block hash. + wallet (bittensor_wallet.Wallet): The wallet to register + netuid (int): The netuid of the subnet to register to. + output_in_place (bool): If true, prints the output in place, otherwise prints to new lines + update_interval (int): The number of nonces to try before checking for more blocks + tpb (int): The number of threads per block. CUDA param that should match the GPU capability + dev_id (Union[list[int], int]): The CUDA device IDs to execute the registration on, either a single device or a + list of devices + n_samples (int): The number of samples of the hash_rate to keep for the EWMA + alpha_ (float): The alpha for the EWMA for the hash_rate calculation + log_verbose (bool): 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. + + Args: + subtensor (bittensor.core.async_subtensor.AsyncSubtensor): The subtensor object to use to get the block number, + difficulty, and block hash. + wallet (bittensor_wallet.Wallet): wallet to use for registration. + netuid (int): The netuid of the subnet to register to. + output_in_place (bool): If true, prints the status in place. Otherwise, prints the status on a new line. + num_processes (Optional[int]): Number of processes to use. + update_interval (Optional[int]): Number of nonces to solve before updating block information. + n_samples (int): The number of samples of the hash_rate to keep for the EWMA + alpha_ (float): The alpha for the EWMA for the hash_rate calculation + log_verbose (bool): 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. + + Args: + subtensor (bittensor.core.async_subtensor.AsyncSubtensor): The subtensor instance. + wallet (bittensor_wallet.Wallet): The wallet to create a proof of work for. + netuid (int): The netuid for the subnet to create a proof of work for. + output_in_place (bool): 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 (bool): If true, uses CUDA to solve the proof of work. + dev_id (Union[list[int], int]): 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 (int): The number of threads per block to use when solving the proof of work. Should be a multiple of 32. + num_processes (int): 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 (int): The number of nonces to run before checking for a new block. + log_verbose (bool): 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: + 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: + 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 new file mode 100644 index 0000000000..078c73d09f --- /dev/null +++ b/bittensor/utils/registration/pow.py @@ -0,0 +1,1182 @@ +"""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. + + Args: + func (function): Function with numpy Input/Output to be decorated. + + Returns: + decorated (function): 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. + + Args: + block_and_hotkey_hash_bytes (bytes): The combined hash bytes of the block and hotkey. + nonce (int): 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. + + Args: + proc_num (int): The number of the process being created. + num_proc (int): The total number of processes running. + update_interval (int): The number of nonces to try to solve before checking for a new block. + finished_queue (multiprocessing.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 (multiprocessing.Queue): The queue to put the solution the process has found during the pow solve. + stopEvent (multiprocessing.Event): 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 (multiprocessing.Array): 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 (multiprocessing.Value): 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 (multiprocessing.Array): 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 (multiprocessing.Lock): The lock to prevent this process from getting the new block data while the + main process is updating the data. + limit (int): 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. + + Arguments: + 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. + + Args: + subtensor (bittensor.core.subtensor.Subtensor): Subtensor instance. + wallet (bittensor_wallet.Wallet): wallet to use for registration. + netuid (int): The netuid of the subnet to register to. + output_in_place (bool): If true, prints the status in place. Otherwise, prints the status on a new line. + num_processes (int): Number of processes to use. + update_interval (int): Number of nonces to solve before updating block information. + n_samples (int): The number of samples of the hash_rate to keep for the EWMA. + alpha_ (float): The alpha for the EWMA for the hash_rate calculation. + log_verbose (bool): 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. + Note: 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. + + Args: + subtensor (bittensor.core.subtensor.Subtensor): The subtensor instance. + netuid (int): The netuid of the network to get the block number, difficulty, and block hash from. + + Returns: + tuple[int, int, bytes] + block_number (int): The current block number. + difficulty (int): The current difficulty of the subnet. + block_hash (bytes): 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. + + Args: + subtensor (bittensor.core.subtensor.Subtensor): The subtensor object to use for getting the current block. + netuid (int): The netuid to use for retrieving the difficulty. + old_block_number (int): The old block number to check against. + hotkey_bytes (bytes): The bytes of the hotkey's pubkey. + curr_diff (multiprocessing.Array): The current difficulty as a multiprocessing array. + curr_block (multiprocessing.Array): Where the current block is stored as a multiprocessing array. + curr_block_num (multiprocessing.Value): Where the current block number is stored as a multiprocessing value. + update_curr_block_ (typing.Callable): A function that updates the current block. + check_block (multiprocessing.Lock): A mp lock that is used to check for a new block. + solvers (list[bittensor.utils.registration.Solver]): A list of solvers to update the current block for. + curr_stats (bittensor.utils.registration.RegistrationStatistics): The current registration statistics to update. + + Returns: + (int) 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. + + Args: + subtensor (bittensor.core.subtensor.Subtensor): The subtensor node to grab blocks. + wallet (bittensor_wallet.Wallet): The wallet to register. + netuid (int): The netuid of the subnet to register to. + output_in_place (bool) If true, prints the output in place, otherwise prints to new lines. + update_interval (int): The number of nonces to try before checking for more blocks. + tpb (int): The number of threads per block. CUDA param that should match the GPU capability + dev_id (Union[list[int], int]): The CUDA device IDs to execute the registration on, either a single device or a + list of devices. + n_samples (int): The number of samples of the hash_rate to keep for the EWMA. + alpha_ (float): The alpha for the EWMA for the hash_rate calculation. + log_verbose (bool): 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. + + Args: + subtensor (bittensor.core.subtensor.Subtensor): The subtensor to create a proof of work for. + wallet (bittensor_wallet.Wallet): The wallet to create a proof of work for. + netuid (int): The netuid for the subnet to create a proof of work for. + output_in_place (bool): If true, prints the progress of the proof of work to the console in-place. Meaning the + progress is printed on the same lines. Default is ``True``. + cuda (bool): If true, uses CUDA to solve the proof of work. Default is ``False``. + dev_id (Union[List[int], int]): 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. Default is ``0``. + tpb (int): The number of threads per block to use when solving the proof of work. Should be a multiple of 32. + Default is ``256``. + num_processes (Optional[int]): 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. Default is None. + update_interval (Optional[int]): The number of nonces to run before checking for a new block. Default is ``None``. + log_verbose (bool): If true, prints the progress of the proof of work more verbosely. Default is ``False``. + + Returns: + Optional[Dict[str, Any]]: 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: + solution: Optional[POWSolution] = _solve_for_difficulty_fast_cuda( + subtensor, + wallet, + netuid=netuid, + output_in_place=output_in_place, + dev_id=dev_id, + tpb=tpb, + update_interval=update_interval, + log_verbose=log_verbose, + ) + else: + solution: Optional[POWSolution] = _solve_for_difficulty_fast( + subtensor, + 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 new file mode 100644 index 0000000000..b46dab9f0c --- /dev/null +++ b/bittensor/utils/registration/register_cuda.py @@ -0,0 +1,124 @@ +"""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. + + Args: + hex_bytes (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. + + Args: + nonce_start (numpy.int64): Starting nonce. + update_interval (numpy.int64): Number of nonces to solve before updating block information. + tpb (int): Threads per block. + block_and_hotkey_hash_bytes (bytes): Keccak(Bytes of the block hash + bytes of the hotkey) 64 bytes. + difficulty (int): Difficulty of the PoW problem. + limit (int): Upper limit of the nonce. + dev_id (int): The CUDA device ID. Defaults to ``0``. + + Returns: + (Union[tuple[Any, bytes], tuple[int, bytes], tuple[Any, None]]): 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/replicate_utils.py b/bittensor/utils/replicate_utils.py deleted file mode 100644 index 2a738e57cb..0000000000 --- a/bittensor/utils/replicate_utils.py +++ /dev/null @@ -1,96 +0,0 @@ -import replicate -import torch -from loguru import logger - -class ReplicateUtility(): - def __init__(self, config): - """ - - Args: - config (Bittensor.Config (Munch)): Bittensor config object - """ - default_neuron_config = vars(config.neuron) - neuron_config = {} - for key in default_neuron_config.keys(): - neuron_config["neuron.{}".format(key)] = default_neuron_config[key] - - self.config = config - self.experiment = replicate.init( - path=self.config.neuron.datapath, - params={ - **self.append_class_prefix(config.neuron, "neuron"), - **self.append_class_prefix(config.synapse, "synapse"), - **self.append_class_prefix(config.axon, "axon"), - **self.append_class_prefix(config.dendrite, "dendrite"), - **self.append_class_prefix(config.metagraph, "metagraph") - } - ) - - def append_class_prefix(self, config, prefix): - """Used primarily to set up parameters of replicate.ai experiments. Forces a dot notation - of the params since replicate.ai ignores it. - - For example, batch_size_train becomes neuron.batch_size_train since batch_size_train is a neuron config. - - Args: - config (Bittensor.Config): Neuron, Synapse, Axon, Dendrite, etc. config objects. - prefix (str): word to prefix to the key. - - Returns: - dict : same config, but with keys prefixed with dot notation. - """ - default_config = vars(config) - prefixed_config = {} - for key in default_config.keys(): - prefixed_config["{}.{}".format(prefix, key)] = default_config[key] - - return prefixed_config - - def checkout_experiment(self, model, best=True): - """ Checks out best (or latest) experiment using Replicate API and returns it for training. - - Args: - model (Torch.nn.Module): Torch model to be loaded - best (bool, optional): Define whether to check out the best - performing version of the model, or the latest version. Defaults to True. - - Raises: - Exception: Generic exception if something fails when checking out. - - Returns: - model (Torch.nn.Module): Torch model to be loaded - """ - try: - experiment = replicate.experiments.get(self.config.session.checkout_experiment) - - latest_experiment = experiment.best() - if not best: - latest_experiment = experiment.latest() - - logger.info("Checking out experiment {} to {}".format( - self.config.session.checkout_experiment, - self.config.neuron.datapath + self.config.neuron.neuron_name)) - - model_file = latest_experiment.open(self.config.neuron.datapath + self.config.neuron.neuron_name + "/model.torch") - checkpt = torch.load(model_file) - model.load_state_dict(checkpt['model']) - except Exception as e: - raise Exception - - return model - - def checkpoint_experiment(self, epoch, **experiment_metrics): - """ Creates a checkpoint for the current experiment object - - Args: - epoch (integer): Current epoch at which we are checkpointing the experiment. - """ - # Create a checkpoint within the experiment. - # This saves the metrics at that point, and makes a copy of the file - # or directory given, which could weights and any other artifacts. - self.experiment.checkpoint( - path=self.config.neuron.datapath + self.config.neuron.neuron_name + "/model.torch", - step=epoch, - metrics=experiment_metrics, - primary_metric=("loss", "minimize"), - ) diff --git a/bittensor/utils/runtime_async_browser.py b/bittensor/utils/runtime_async_browser.py new file mode 100644 index 0000000000..4abee8da42 --- /dev/null +++ b/bittensor/utils/runtime_async_browser.py @@ -0,0 +1,322 @@ +import asyncio +import bittensor as bt +from .btlogging import logging + +# Default network in case user does not explicitly initialize runtime browser. +DEFAULT_NETWORK='local' + +TYPE='ty' # depending on where we get metadata from, we have 'ty' or 'type', TODO: settle on metadata source + +class runtime_async_browser_imp(type): + async_subtensor = None + pallets = None + apis = None + subtensor_metadata = None + block = None # block used to initialize runtime and get metadata + network = None + items = {} + + # Support init through bt.runtime(network=...,block=...) + def __call__(cls,**kwargs): + cls.init(**kwargs) + return cls + + def init(cls,network=None,async_subtensor=None,block=None): + cls.block = block + if async_subtensor: + if async_subtensor != cls.async_subtensor: + cls.async_subtensor = async_subtensor + cls.network = None + cls.items.clear() + else: + if not network: + logging.warning(f"Initializing runtime browser with default network {DEFAULT_NETWORK}, block={block}.") + network = DEFAULT_NETWORK + if network != cls.network: + cls.items.clear() + cls.network = network + cls.async_subtensor = bt.async_subtensor(network=network) + + def get_async_subtensor(cls): + if cls.async_subtensor is None: + cls.init() + return cls.async_subtensor + + async def init_metadata(cls): + async_subtensor = cls.get_async_subtensor() + async_substrate = async_subtensor.substrate + if cls.block: + block = cls.block + else: + block = await async_subtensor.block + block_hash = await async_substrate.get_block_hash(block) + rt = await async_substrate.init_runtime(block_hash=block_hash) + if 1: + cls.subtensor_metadata = rt.metadata_v15.value() + else: + # not sure if this is the most future proof way to go to get to the metadata + md_dict = substrate.metadata.value[1] + version = list(md_dict.keys())[0] + cls.subtensor_metadata = md_dict[version] + cls.subtensor_metadata['apis'] = rt.metadata_v15.value()['apis'] + + # blocking at first call + def get_metadata(cls): + if cls.subtensor_metadata is None: + raise Exception(f'Metadata not initialized; call await bittensor.async_runtime.init_metadata() before using async_runtime') + return cls.subtensor_metadata + + # blocking at first call + def get_pallets(cls): + if cls.pallets is None: + md = cls.get_metadata() + cls.pallets = {p.get('name','?'):p for p in md['pallets']} + return cls.pallets + + # blocking at first call + def get_apis(cls): + if cls.apis is None: + #cls.apis = list(bt.core.settings.TYPE_REGISTRY['runtime_api'].keys()) + md = cls.get_metadata() + cls.apis = [item['name'] for item in md['apis']] + return cls.apis + + def dir(cls): + pallets = cls.get_pallets() + apis = cls.get_apis() + return list(pallets.keys())+apis + + # We cannot make __getattr__ async, or the end user would have to await the + # module _and_ the item within the module, that is, we don't want this: + # await (await bt.async_runtime.SubtensorModule).NetworkLastRegistered() + # So necessarily the first invocation will be blocking, as it needs to + # fetch metadata. + def __getattr__(cls,item): + if item == '__super__': + return object + if not item[0].isalpha(): + try: + ret = cls.__super__.__getattr__(item) + except Exception as e: + raise + return ret + if item == 'metadata': + return cls.subtensor_metadata + if item in cls.get_pallets(): + if not item in cls.items: + cls.items[item] = subbrowser_module( + subbrowser=cls, + module=item, + metadata=cls.get_pallets()[item], + ) + return cls.items[item] + apis = {} + try: + apis = cls.get_apis() + except Exception as e: + logging.warning(f'Error fetching apis - this is an internal bittensor.runtime error') + + if item in apis: + if not item in cls.items: + cls.items[item] = subbrowser_api(subbrowser=cls,api=item) + else: + raise Exception(f"Pallet or API {item} not found; use bittensor.runtime.dir() to get valid options") + return cls.items[item] + + def __repr__(cls): + return f'' + +# this makes bittensor.runtime behave like an object, without prior instantiation +class runtime_async_browser(metaclass=runtime_async_browser_imp): + def __dir__(): + return bt.runtime.dir() + +# proxy for subtensor module, e.g. bt.runtime.SubtensorModule or bt.runtime.System +class subbrowser_module: + _subbrowser = None + _module = None + _objs = None + _storage_entries = None + _constants = None + _metadata = None + + def __init__(self,subbrowser=None,module=None,metadata=None): + self._subbrowser = subbrowser + self._module = module + self._objs = {} + self._metadata = metadata + self._storage_entries = {item.get('name','?'):item for item in metadata['storage']['entries']} + self._constants = {item.get('name','?'):item for item in metadata['constants']} + + def dir(self): + return list(self._constants.keys())+list(self._storage_entries.keys()) + + def __getattr__(self,name): + if not name in self._objs: + md = tp = None + if name in self._storage_entries: + md = self._storage_entries[name] + tp = 'storage' + elif name in self._constants: + md = self._constants[name] + tp = 'constant' + else: + msg = f'Storage entry or constant "{name}" not found; available are: ' + msg += 'storage entries '+', '.join(self._storage_entries.keys()) + msg += ' and constants '+', '.join(self._constants.keys()) + msg += '\nPossibly the entry exists on a different block height; explicitly initialize on a block using e.g. bt.runtime.init(block=4000000)' + # TODO: implement a delayed error object, so that you can call bt.runtime.SubtensorModule.SubnetLimit(block=4000000) after SubnetLimit does not exist anymore? + raise Exception(msg) + # It is a design decision to not return the value of plain types, but still return an + # object, that can be queried e.g. like bt.runtime.Timestamp.Now(), because it keeps + # the option open to add block=... kwarg. + # The alternative is to test 'Plain' in md[TYPE] and perform the query here. + self._objs[name] = subbrowser_objproxy( + subbrowser_module=self, + name=name, + metadata=md, + tp=tp, + ) + return self._objs[name] + + def __repr__(self): + return f'' + +# proxy for maps and singular elements, either constants or storage entries e.g. +# bt.runtime.Timestamp.Now (having no index) +# bt.runtime.SubtensorModule.LastAdjustmentBlock (having 1D index: netuid) +# bt.runtime.System.Account (having 1D index: coldkey_ss58) +# bt.runtime.Commitments.RateLimit (constant, no index) +# accept: +# obj() +# obj.query() +# obj[29] +# obj(29) +# obj.query(29) +# and for the call-like interfaces, kwargs: +# obj(block=12345) +# obj.query(block=12345) +# obj(29,block=12345) +# obj.query(29,block=12345) +class subbrowser_objproxy: + _subbrowser_module = None + _name = None + _metadata = None + _type = None + _n_indices = None + + def __init__(self,subbrowser_module=None,name=None,metadata=None,tp=None): + self._name = name + self._subbrowser_module = subbrowser_module + self._metadata = metadata + self._fullname = self._subbrowser_module._module+'.'+self._name + self._type = tp + self._n_indices = self._get_n_indices() + + def _get_n_indices(self): + if type(self._metadata[TYPE]) is not dict: + return 0 + if 'Plain' in self._metadata[TYPE]: + return 0 + if 'Map' in self._metadata[TYPE]: + mapinfo = self._metadata[TYPE]['Map'] + hashers = mapinfo.get('hashers',[]) + return len(hashers) + + async def _query(self,*args,**kwargs): + if len(args) != self._n_indices: + if self._n_indices == 0: + raise Exception(f'plain item {self._fullname} does not accept indices; use {self._fullname}()') + raise Exception(f'map {self._fullname} requires {self._n_indices} indices') + if type(self._metadata[TYPE]) is not dict: + pass + elif 'Plain' in self._metadata[TYPE]: + pass + elif 'Map' in self._metadata[TYPE]: + # Specifically ensure that we enfore ss58 indices, showing pretty errors. + mapinfo = self._metadata[TYPE]['Map'] + hashers = mapinfo.get('hashers',[]) + for i,h in enumerate(hashers): + if h in ('Blake2_128Concat'): # in Stake "Identity" is an ss58 addr, in Alpha and other maps, "Identity" is an integer. + if type(args[i]) != str or len(args[i]) != 48: + raise Exception(f'index {i} of {self._fullname} should be an ss58 address') + if self._type == 'storage': + ret = await self._subbrowser_module._subbrowser.async_subtensor.query_module( + module=self._subbrowser_module._module, + name=self._name, + params=args, + block=kwargs.get('block',self._subbrowser_module._subbrowser.block) + ) + # don't even try to understand the logic of what query_module() returns; might be scale obj, str or int + try: + return ret.value + except: + return ret + elif self._type == 'constant': + ret = await self._subbrowser_module._subbrowser.async_subtensor.query_constant( + module_name=self._subbrowser_module._module, + constant_name=self._name, + block=kwargs.get('block',self._subbrowser_module._subbrowser.block) + ) + try: + return ret.value + except: + return ret + + async def __call__(self,*args,**kwargs): + #logging.warning(f'objproxy call like access: {args} // {kwargs}') + return await self._query(*args,**kwargs) + + def __getitem__(self,args): + logging.warning(f'indexed querying of runtime objects is officially not async, this may break in the future; use object(index0,...) instead of object[index0,...]') + # TODO: test if it might still work anyway to return a coroutine; we're not performing del or assigning values anyway + if type(args) != tuple: args = (args,) + return self._query(*args) + + def __repr__(self): + if self._n_indices>0: + indices = ','.join(f'index{i}' for i in range(self._n_indices)) + return f'' + return f'' + +# proxy for subtensor api, e.g. in order to call: +# bt.runtime.SubnetRegistrationRuntimeApi.get_network_registration_cost() +# bt.runtime.SubnetRegistrationRuntimeApi.get_network_registration_cost(block=12345) +class subbrowser_api: + _subbrowser = None + _api = None + _calls = None + def __init__(self,subbrowser=None,api=None): + self._subbrowser = subbrowser + self._api = api + self._calls = {} + self._methods = None + md = self._subbrowser.get_metadata() + for item in md['apis']: + if item['name'] == api: + self._methods = [m['name'] for m in item['methods']] + if self._methods is None: + logging.warning(f'Failed to get methods for api {api} from metadata') + self._methods = [] + + def dir(self): + #return list(bt.core.settings.TYPE_REGISTRY['runtime_api'][self._api]["methods"].keys()) + return self._methods + + def __getattr__(self,call): + if not call in self._calls: + #methods = bt.core.settings.TYPE_REGISTRY["runtime_api"][self._api]["methods"] + if not call in self._methods: + raise Exception(f'API call {self._api}.{call} not found; available calls are {", ".join(self._methods)}') + async def query(*args,block=None): + return await self._subbrowser.async_subtensor.query_runtime_api( + self._api, + call, + args, + block=block or self._subbrowser.block, + ) + self._calls[call] = query + return self._calls[call] + + def __repr__(self): + return f'' diff --git a/bittensor/utils/runtime_browser.py b/bittensor/utils/runtime_browser.py new file mode 100644 index 0000000000..ea7adba1bb --- /dev/null +++ b/bittensor/utils/runtime_browser.py @@ -0,0 +1,304 @@ +import bittensor as bt +from .btlogging import logging + +# Default network in case user does not explicitly initialize runtime browser. +DEFAULT_NETWORK='local' + +TYPE='ty' # depending on where we get metadata from, we have 'ty' or 'type', TODO: settle on metadata source + +class runtime_browser_imp(type): + subtensor = None + pallets = None + apis = None + subtensor_metadata = None + block = None # block used to initialize runtime and get metadata + network = None + items = {} + + # Support init through bt.runtime(network=...,block=...) + def __call__(cls,**kwargs): + cls.init(**kwargs) + return cls + + def init(cls,network=None,subtensor=None,block=None): + cls.block = block + if subtensor: + if subtensor != cls.subtensor: + cls.subtensor = subtensor + cls.network = None + cls.items.clear() + else: + if not network: + logging.warning(f"Initializing runtime browser with default network {DEFAULT_NETWORK}, block={block}.") + network = DEFAULT_NETWORK + if network != cls.network: + cls.items.clear() + cls.network = network + cls.subtensor = bt.subtensor(network=network) + + def get_subtensor(cls): + if cls.subtensor is None: + cls.init() + return cls.subtensor + + def get_metadata(cls): + if cls.subtensor_metadata is None: + subtensor = cls.get_subtensor() + substrate = subtensor.substrate + block = cls.block or subtensor.block + block_hash = substrate.get_block_hash(block) + rt = substrate.init_runtime(block_hash=block_hash) + if 1: + cls.subtensor_metadata = rt.metadata_v15.value() + else: + # not sure if this is the most future proof way to go to get to the metadata + md_dict = substrate.metadata.value[1] + version = list(md_dict.keys())[0] + cls.subtensor_metadata = md_dict[version] + cls.subtensor_metadata['apis'] = rt.metadata_v15.value()['apis'] + return cls.subtensor_metadata + + def get_pallets(cls): + if cls.pallets is None: + md = cls.get_metadata() + cls.pallets = {p.get('name','?'):p for p in md['pallets']} + return cls.pallets + + def get_apis(cls): + if cls.apis is None: + #cls.apis = list(bt.core.settings.TYPE_REGISTRY['runtime_api'].keys()) + md = cls.get_metadata() + cls.apis = [item['name'] for item in md['apis']] + return cls.apis + + def dir(cls): + return list(cls.get_pallets().keys())+cls.get_apis() + + def __getattr__(cls,item): + if item == '__super__': + return object + if not item[0].isalpha(): + try: + ret = cls.__super__.__getattr__(item) + except Exception as e: + raise + return ret + if item == 'metadata': + return cls.subtensor_metadata + if item in cls.get_pallets(): + if not item in cls.items: + cls.items[item] = subbrowser_module( + subbrowser=cls, + module=item, + metadata=cls.get_pallets()[item], + ) + return cls.items[item] + apis = {} + try: + apis = cls.get_apis() + except Exception as e: + logging.warning(f'Error fetching apis - this is an internal bittensor.runtime error') + + if item in apis: + if not item in cls.items: + cls.items[item] = subbrowser_api(subbrowser=cls,api=item) + else: + raise Exception(f"Pallet or API {item} not found; use bittensor.runtime.dir() to get valid options") + return cls.items[item] + + def __repr__(cls): + return f'' + +# this makes bittensor.runtime behave like an object, without prior instantiation +class runtime_browser(metaclass=runtime_browser_imp): + def __dir__(): + return bt.runtime.dir() + +# proxy for subtensor module, e.g. bt.runtime.SubtensorModule or bt.runtime.System +class subbrowser_module: + _subbrowser = None + _module = None + _objs = None + _storage_entries = None + _constants = None + _metadata = None + + def __init__(self,subbrowser=None,module=None,metadata=None): + self._subbrowser = subbrowser + self._module = module + self._objs = {} + self._metadata = metadata + self._storage_entries = {item.get('name','?'):item for item in metadata['storage']['entries']} + self._constants = {item.get('name','?'):item for item in metadata['constants']} + + def dir(self): + return list(self._constants.keys())+list(self._storage_entries.keys()) + + def __getattr__(self,name): + if not name in self._objs: + md = tp = None + if name in self._storage_entries: + md = self._storage_entries[name] + tp = 'storage' + elif name in self._constants: + md = self._constants[name] + tp = 'constant' + else: + msg = f'Storage entry or constant "{name}" not found; available are: ' + msg += 'storage entries '+', '.join(self._storage_entries.keys()) + msg += ' and constants '+', '.join(self._constants.keys()) + msg += '\nPossibly the entry exists on a different block height; explicitly initialize on a block using e.g. bt.runtime.init(block=4000000)' + # TODO: implement a delayed error object, so that you can call bt.runtime.SubtensorModule.SubnetLimit(block=4000000) after SubnetLimit does not exist anymore? + raise Exception(msg) + # It is a design decision to not return the value of plain types, but still return an + # object, that can be queried e.g. like bt.runtime.Timestamp.Now(), because it keeps + # the option open to add block=... kwarg. + # The alternative is to test 'Plain' in md[TYPE] and perform the query here. + self._objs[name] = subbrowser_objproxy( + subbrowser_module=self, + name=name, + metadata=md, + tp=tp, + ) + return self._objs[name] + + def __repr__(self): + return f'' + +# proxy for maps and singular elements, either constants or storage entries e.g. +# bt.runtime.Timestamp.Now (having no index) +# bt.runtime.SubtensorModule.LastAdjustmentBlock (having 1D index: netuid) +# bt.runtime.System.Account (having 1D index: coldkey_ss58) +# bt.runtime.Commitments.RateLimit (constant, no index) +# accept: +# obj() +# obj.query() +# obj[29] +# obj(29) +# obj.query(29) +# and for the call-like interfaces, kwargs: +# obj(block=12345) +# obj.query(block=12345) +# obj(29,block=12345) +# obj.query(29,block=12345) +class subbrowser_objproxy: + _subbrowser_module = None + _name = None + _metadata = None + _type = None + _n_indices = None + + def __init__(self,subbrowser_module=None,name=None,metadata=None,tp=None): + self._name = name + self._subbrowser_module = subbrowser_module + self._metadata = metadata + self._fullname = self._subbrowser_module._module+'.'+self._name + self._type = tp + self._n_indices = self._get_n_indices() + + def _get_n_indices(self): + if type(self._metadata[TYPE]) is not dict: + return 0 + if 'Plain' in self._metadata[TYPE]: + return 0 + if 'Map' in self._metadata[TYPE]: + mapinfo = self._metadata[TYPE]['Map'] + hashers = mapinfo.get('hashers',[]) + return len(hashers) + + def _query(self,*args,**kwargs): + if len(args) != self._n_indices: + if self._n_indices == 0: + raise Exception(f'plain item {self._fullname} does not accept indices; use {self._fullname}()') + raise Exception(f'map {self._fullname} requires {self._n_indices} indices') + if type(self._metadata[TYPE]) is not dict: + pass + elif 'Plain' in self._metadata[TYPE]: + pass + elif 'Map' in self._metadata[TYPE]: + # Specifically ensure that we enfore ss58 indices, showing pretty errors. + mapinfo = self._metadata[TYPE]['Map'] + hashers = mapinfo.get('hashers',[]) + for i,h in enumerate(hashers): + if h in ('Blake2_128Concat'): # in Stake "Identity" is an ss58 addr, in Alpha and other maps, "Identity" is an integer. + if type(args[i]) != str or len(args[i]) != 48: + raise Exception(f'index {i} of {self._fullname} should be an ss58 address') + if self._type == 'storage': + b = kwargs.get('block',self._subbrowser_module._subbrowser.block) + ret = self._subbrowser_module._subbrowser.subtensor.query_module( + module=self._subbrowser_module._module, + name=self._name, + params=args, + block=b + ) + # don't even try to understand the logic of what query_module() returns; might be scale obj, str or int + try: + return ret.value + except: + return ret + elif self._type == 'constant': + ret = self._subbrowser_module._subbrowser.subtensor.query_constant( + module_name=self._subbrowser_module._module, + constant_name=self._name, + block=kwargs.get('block',self._subbrowser_module._subbrowser.block) + ).value + # don't even try to understand the logic of what query_module() returns; might be scale obj, str or int + try: + return ret.value + except: + return ret + + def __call__(self,*args,**kwargs): + return self._query(*args,**kwargs) + + def __getitem__(self,args): + if type(args) != tuple: args = (args,) + return self._query(*args) + + def __repr__(self): + if self._n_indices>0: + indices = ','.join(f'index{i}' for i in range(self._n_indices)) + return f'' + return f'' + +# proxy for subtensor api, e.g. in order to call: +# bt.runtime.SubnetRegistrationRuntimeApi.get_network_registration_cost() +# bt.runtime.SubnetRegistrationRuntimeApi.get_network_registration_cost(block=12345) +class subbrowser_api: + _subbrowser = None + _api = None + _calls = None + def __init__(self,subbrowser=None,api=None): + self._subbrowser = subbrowser + self._api = api + self._calls = {} + self._methods = None + md = self._subbrowser.get_metadata() + for item in md['apis']: + if item['name'] == api: + self._methods = [m['name'] for m in item['methods']] + if self._methods is None: + logging.warning(f'Failed to get methods for api {api} from metadata') + self._methods = [] + + def dir(self): + #return list(bt.core.settings.TYPE_REGISTRY['runtime_api'][self._api]["methods"].keys()) + return self._methods + + def __getattr__(self,call): + if not call in self._calls: + #methods = bt.core.settings.TYPE_REGISTRY["runtime_api"][self._api]["methods"] + if not call in self._methods: + raise Exception(f'API call {self._api}.{call} not found; available calls are {", ".join(self._methods)}') + def query(*args,block=None): + return self._subbrowser.subtensor.query_runtime_api( + self._api, + call, + args, + block=block or self._subbrowser.block, + ) + self._calls[call] = query + return self._calls[call] + + def __repr__(self): + return f'' diff --git a/bittensor/utils/subnets.py b/bittensor/utils/subnets.py new file mode 100644 index 0000000000..d92a4f58f3 --- /dev/null +++ b/bittensor/utils/subnets.py @@ -0,0 +1,60 @@ +from abc import ABC, abstractmethod +from typing import Any, Union, Optional, TYPE_CHECKING + +from bittensor.core.axon import Axon +from bittensor.core.dendrite import Dendrite +from bittensor.utils.btlogging import logging + +# For annotation purposes +if TYPE_CHECKING: + from bittensor_wallet import Wallet + from bittensor.core.synapse import Synapse + + +# Community uses this class +class SubnetsAPI(ABC): + """This class is not used within the bittensor package, but is actively used by the community.""" + + def __init__(self, wallet: "Wallet"): + self.wallet = wallet + self.dendrite = Dendrite(wallet=wallet) + + async def __call__(self, *args, **kwargs): + return await self.query_api(*args, **kwargs) + + @abstractmethod + def prepare_synapse(self, *args, **kwargs) -> Any: + """Prepare the synapse-specific payload.""" + + @abstractmethod + def process_responses(self, responses: list[Union["Synapse", Any]]) -> Any: + """Process the responses from the network.""" + + async def query_api( + self, + axons: Union["Axon", list["Axon"]], + deserialize: Optional[bool] = False, + timeout: Optional[int] = 12, + **kwargs, + ) -> Any: + """ + Queries the API nodes of a subnet using the given synapse and bespoke query function. + + Args: + axons (Union[bt.axon, list[bt.axon]]): The list of axon(s) to query. + deserialize (Optional[bool]): Whether to deserialize the responses. Defaults to False. + timeout (Optional[int]): The timeout in seconds for the query. Defaults to 12. + **kwargs: Keyword arguments for the prepare_synapse_fn. + + Returns: + Any: The result of the process_responses_fn. + """ + synapse = self.prepare_synapse(**kwargs) + logging.debug(f"Querying validator axons with synapse {synapse.name}...") + responses = await self.dendrite( + axons=axons, + synapse=synapse, + deserialize=deserialize, + timeout=timeout, + ) + return self.process_responses(responses) diff --git a/bittensor/synapses/__init__.py b/bittensor/utils/substrate_utils/__init__.py similarity index 100% rename from bittensor/synapses/__init__.py rename to bittensor/utils/substrate_utils/__init__.py diff --git a/bittensor/utils/substrate_utils/hasher.py b/bittensor/utils/substrate_utils/hasher.py new file mode 100644 index 0000000000..0075bb69dd --- /dev/null +++ b/bittensor/utils/substrate_utils/hasher.py @@ -0,0 +1,62 @@ +"""Helper functions used to calculate keys for Substrate storage items""" + +from hashlib import blake2b + +import xxhash + + +def blake2_256(data): + """ + Helper function to calculate a 32 bytes Blake2b hash for provided data, used as key for Substrate storage items + """ + return blake2b(data, digest_size=32).digest() + + +def blake2_128(data): + """ + Helper function to calculate a 16 bytes Blake2b hash for provided data, used as key for Substrate storage items + """ + return blake2b(data, digest_size=16).digest() + + +def blake2_128_concat(data): + """ + Helper function to calculate a 16 bytes Blake2b hash for provided data, concatenated with data, used as key + for Substrate storage items + """ + return blake2b(data, digest_size=16).digest() + data + + +def xxh128(data): + """ + Helper function to calculate a 2 concatenated xxh64 hash for provided data, used as key for several Substrate + """ + storage_key1 = bytearray(xxhash.xxh64(data, seed=0).digest()) + storage_key1.reverse() + + storage_key2 = bytearray(xxhash.xxh64(data, seed=1).digest()) + storage_key2.reverse() + + return storage_key1 + storage_key2 + + +def two_x64_concat(data): + """ + Helper function to calculate a xxh64 hash with concatenated data for provided data, + used as key for several Substrate + """ + storage_key = bytearray(xxhash.xxh64(data, seed=0).digest()) + storage_key.reverse() + + return storage_key + data + + +def xxh64(data): + storage_key = bytearray(xxhash.xxh64(data, seed=0).digest()) + storage_key.reverse() + + return storage_key + + +def identity(data): + return data diff --git a/bittensor/utils/substrate_utils/storage.py b/bittensor/utils/substrate_utils/storage.py new file mode 100644 index 0000000000..315f7e44c8 --- /dev/null +++ b/bittensor/utils/substrate_utils/storage.py @@ -0,0 +1,277 @@ +import binascii +from typing import Any, Optional + +from scalecodec import ScaleBytes, GenericMetadataVersioned, ss58_decode +from scalecodec.base import ScaleDecoder, RuntimeConfigurationObject, ScaleType + +from bittensor.core.errors import StorageFunctionNotFound +from bittensor.utils.substrate_utils.hasher import ( + blake2_256, + two_x64_concat, + xxh128, + blake2_128, + blake2_128_concat, + identity, +) + + +class StorageKey: + """ + A StorageKey instance is a representation of a single state entry. + + Substrate uses a simple key-value data store implemented as a database-backed, modified Merkle tree. + All of Substrate's higher-level storage abstractions are built on top of this simple key-value store. + """ + + def __init__( + self, + pallet: Optional[str], + storage_function: Optional[str], + params: Optional[list], + data: Optional[bytes], + value_scale_type: Optional[str], + metadata: GenericMetadataVersioned, + runtime_config: RuntimeConfigurationObject, + ): + self.pallet = pallet + self.storage_function = storage_function + self.params = params + self.params_encoded = [] + self.data = data + self.metadata = metadata + self.runtime_config = runtime_config + self.value_scale_type = value_scale_type + self.metadata_storage_function = None + + @classmethod + def create_from_data( + cls, + data: bytes, + runtime_config: RuntimeConfigurationObject, + metadata: GenericMetadataVersioned, + value_scale_type: str = None, + pallet: str = None, + storage_function: str = None, + ) -> "StorageKey": + """ + Create a StorageKey instance providing raw storage key bytes + + Args: + data: bytes representation of the storage key + runtime_config: RuntimeConfigurationObject + metadata: GenericMetadataVersioned + value_scale_type: type string of to decode result data + pallet: name of pallet + storage_function: name of storage function + + Returns: + StorageKey + """ + if not value_scale_type and pallet and storage_function: + metadata_pallet = metadata.get_metadata_pallet(pallet) + + if not metadata_pallet: + raise StorageFunctionNotFound(f'Pallet "{pallet}" not found') + + storage_item = metadata_pallet.get_storage_function(storage_function) + + if not storage_item: + raise StorageFunctionNotFound( + f'Storage function "{pallet}.{storage_function}" not found' + ) + + # Process specific type of storage function + value_scale_type = storage_item.get_value_type_string() + + return cls( + pallet=None, + storage_function=None, + params=None, + data=data, + metadata=metadata, + value_scale_type=value_scale_type, + runtime_config=runtime_config, + ) + + @classmethod + def create_from_storage_function( + cls, + pallet: str, + storage_function: str, + params: list, + runtime_config: RuntimeConfigurationObject, + metadata: GenericMetadataVersioned, + ) -> "StorageKey": + """ + Create a StorageKey instance providing storage function details + + Args: + pallet: name of pallet + storage_function: name of storage function + params: Optional list of parameters in case of a Mapped storage function + runtime_config: RuntimeConfigurationObject + metadata: GenericMetadataVersioned + + Returns: + StorageKey + """ + storage_key_obj = cls( + pallet=pallet, + storage_function=storage_function, + params=params, + data=None, + runtime_config=runtime_config, + metadata=metadata, + value_scale_type=None, + ) + + storage_key_obj.generate() + + return storage_key_obj + + def convert_storage_parameter(self, scale_type: str, value: Any): + if type(value) is bytes: + value = f"0x{value.hex()}" + + if scale_type == "AccountId": + if value[0:2] != "0x": + return "0x{}".format( + ss58_decode(value, self.runtime_config.ss58_format) + ) + + return value + + def to_hex(self) -> Optional[str]: + """ + Returns a Hex-string representation of current StorageKey data + + Returns: + Hex string + """ + if self.data: + return f"0x{self.data.hex()}" + + def generate(self) -> bytes: + """ + Generate a storage key for current specified pallet/function/params + """ + + # Search storage call in metadata + metadata_pallet = self.metadata.get_metadata_pallet(self.pallet) + + if not metadata_pallet: + raise StorageFunctionNotFound(f'Pallet "{self.pallet}" not found') + + self.metadata_storage_function = metadata_pallet.get_storage_function( + self.storage_function + ) + + if not self.metadata_storage_function: + raise StorageFunctionNotFound( + f'Storage function "{self.pallet}.{self.storage_function}" not found' + ) + + # Process specific type of storage function + self.value_scale_type = self.metadata_storage_function.get_value_type_string() + param_types = self.metadata_storage_function.get_params_type_string() + + hashers = self.metadata_storage_function.get_param_hashers() + + storage_hash = xxh128( + metadata_pallet.value["storage"]["prefix"].encode() + ) + xxh128(self.storage_function.encode()) + + # Encode parameters + self.params_encoded = [] + if self.params: + for idx, param in enumerate(self.params): + if type(param) is ScaleBytes: + # Already encoded + self.params_encoded.append(param) + else: + param = self.convert_storage_parameter(param_types[idx], param) + param_obj = self.runtime_config.create_scale_object( + type_string=param_types[idx] + ) + self.params_encoded.append(param_obj.encode(param)) + + for idx, param in enumerate(self.params_encoded): + # Get hasher associated with param + try: + param_hasher = hashers[idx] + except IndexError: + raise ValueError(f"No hasher found for param #{idx + 1}") + + params_key = bytes() + + # Convert param to bytes + if type(param) is str: + params_key += binascii.unhexlify(param) + elif type(param) is ScaleBytes: + params_key += param.data + elif isinstance(param, ScaleDecoder): + params_key += param.data.data + + if not param_hasher: + param_hasher = "Twox128" + + if param_hasher == "Blake2_256": + storage_hash += blake2_256(params_key) + + elif param_hasher == "Blake2_128": + storage_hash += blake2_128(params_key) + + elif param_hasher == "Blake2_128Concat": + storage_hash += blake2_128_concat(params_key) + + elif param_hasher == "Twox128": + storage_hash += xxh128(params_key) + + elif param_hasher == "Twox64Concat": + storage_hash += two_x64_concat(params_key) + + elif param_hasher == "Identity": + storage_hash += identity(params_key) + + else: + raise ValueError('Unknown storage hasher "{}"'.format(param_hasher)) + + self.data = storage_hash + + return self.data + + def decode_scale_value(self, data: Optional[ScaleBytes] = None) -> ScaleType: + result_found = False + + if data is not None: + change_scale_type = self.value_scale_type + result_found = True + elif self.metadata_storage_function.value["modifier"] == "Default": + # Fallback to default value of storage function if no result + change_scale_type = self.value_scale_type + data = ScaleBytes( + self.metadata_storage_function.value_object["default"].value_object + ) + else: + # No result is interpreted as an Option<...> result + change_scale_type = f"Option<{self.value_scale_type}>" + data = ScaleBytes( + self.metadata_storage_function.value_object["default"].value_object + ) + + # Decode SCALE result data + updated_obj = self.runtime_config.create_scale_object( + type_string=change_scale_type, data=data, metadata=self.metadata + ) + updated_obj.decode() + updated_obj.meta_info = {"result_found": result_found} + + return updated_obj + + def __repr__(self): + if self.pallet and self.storage_function: + return f"" + elif self.data: + return f"" + else: + return repr(self) diff --git a/bittensor/utils/test_runtime_async_browser.py b/bittensor/utils/test_runtime_async_browser.py new file mode 100755 index 0000000000..e76248ce7a --- /dev/null +++ b/bittensor/utils/test_runtime_async_browser.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python + +# testing code for runtime async browser + +import bittensor as bt +import asyncio +import sys + +calls = [l for l in ''' +# subnet registration info +await bt.async_runtime.SubtensorModule.NetworkLastRegistered() +await bt.async_runtime.SubtensorModule.NetworkLastLockCost() +await bt.async_runtime.SubtensorModule.NetworkImmunityPeriod() +await bt.async_runtime.SubnetRegistrationRuntimeApi.get_network_registration_cost() +await bt.async_runtime.SubtensorModule.SubnetOwner[29] +await bt.async_runtime.SubtensorModule.SubnetLocked[29] +await bt.async_runtime.SubtensorModule.NetworkRegisteredAt[29] +await bt.async_runtime.SubtensorModule.NetworkLastLockCost(block=bt.runtime.SubtensorModule.NetworkRegisteredAt[29]) +await bt.async_runtime.SubtensorModule.NetworkLastLockCost(block=bt.runtime.SubtensorModule.NetworkRegisteredAt[29]-1) +await bt.async_runtime.SubnetRegistrationRuntimeApi.get_network_registration_cost(block=bt.runtime.SubtensorModule.NetworkRegisteredAt[29]) +await bt.async_runtime.SubnetRegistrationRuntimeApi.get_network_registration_cost(block=bt.runtime.SubtensorModule.NetworkRegisteredAt[29]-1) +# hotkey registration and commitment info +await bt.async_runtime.SubtensorModule.NetworkRegistrationAllowed[29] +await bt.async_runtime.SubtensorModule.Burn(9) +await bt.async_runtime.SubtensorModule.Burn(29) +await bt.async_runtime.SubtensorModule.BlockAtRegistration[29,5] +await bt.async_runtime.SubtensorModule.AdjustmentInterval[29] +await bt.async_runtime.SubtensorModule.LastAdjustmentBlock[29] +await bt.async_runtime.SubtensorModule.LastAdjustmentBlock(29,block=6000000) +await bt.async_runtime.SubtensorModule.ImmunityPeriod[29] +await bt.async_runtime.Commitments.LastCommitment[3, '5HTRBzc7CZvASdVZc7Fwzqnp1jXeP9z3KpoRKDZWhYSAksKq'] +await bt.async_runtime.Commitments.CommitmentOf[3, '5HTRBzc7CZvASdVZc7Fwzqnp1jXeP9z3KpoRKDZWhYSAksKq'] +await bt.async_runtime.Commitments.MaxFields() +await bt.async_runtime.SubtensorModule.Weights[0,1] +await bt.async_runtime.SubtensorModule.Weights(0,1) +await bt.async_runtime.SubtensorModule.Weights(0,1,block=6000000) +await bt.async_runtime.SubtensorModule.TotalHotkeyAlpha['5HmmmmhaoDbmt4KcYmAXETvfGBwFmcNgVCYSBH5JZRv6wrFf',29] +await bt.async_runtime.SubtensorModule.TotalHotkeyAlpha('5HmmmmhaoDbmt4KcYmAXETvfGBwFmcNgVCYSBH5JZRv6wrFf',29,block=6000000) +# various +await bt.async_runtime.Timestamp.Now() +await bt.async_runtime.Timestamp.Now(block=6000000) +await bt.async_runtime.Timestamp.Now(block=5900000) +(await bt.async_runtime.SubtensorModule.Alpha['5HmmmmhaoDbmt4KcYmAXETvfGBwFmcNgVCYSBH5JZRv6wrFf','5HHHHHzgLnYRvnKkHd45cRUDMHXTSwx7MjUzxBrKbY4JfZWn',29])['bits']/(1<<64) +(await bt.async_runtime.SubtensorModule.Alpha['5HHHHHzgLnYRvnKkHd45cRUDMHXTSwx7MjUzxBrKbY4JfZWn','5HHHHHzgLnYRvnKkHd45cRUDMHXTSwx7MjUzxBrKbY4JfZWn',29])['bits']/(1<<64) +await bt.async_runtime.System.Account['5HHHHHzgLnYRvnKkHd45cRUDMHXTSwx7MjUzxBrKbY4JfZWn'] +await bt.async_runtime.SubtensorModule.Owner['5GuNCPiUQBVoXCJgrMeXmc7rVjwT5zUEK5RzjhGmVqbQA5me'] +await bt.async_runtime.Proxy.Proxies['5Enrkn6rRLGz4tK3TWamnbKYECKpz3hffNk9RBaDC426jLgb'] +# init runtime interface with particular fixed block: +await bt.async_runtime.System.Number() +await bt.async_runtime(block=6000000).System.Number() +await bt.async_runtime.System.Number() +await bt.async_runtime(block=None).System.Number() +# init runtime interface with particular network: +await bt.async_runtime(network='test').System.Number() + +# exceptions, containing usage help: +await bt.async_runtime.NonExistantPallet[0] +await bt.async_runtime.System.NonExistentItem[0] +await bt.async_runtime.SubnetRegistrationRuntimeApi.no_such_call() +await bt.async_runtime.Timestamp.Now[1] +await bt.async_runtime.SubtensorModule.AdjustmentInterval[0,0] +await bt.async_runtime.System.Account() +await bt.async_runtime.System.Account[0] +await bt.async_runtime.System.Account['abc'] +await bt.async_runtime.System.Account['5HHHHHzgLnYRvnKkHd45cRUDMHXTSwx7MjUzxBrKbY4JfZWn',0] + +# exposed metadata, lists and __repr__ containing usage help: +bt.async_runtime +bt.async_runtime.dir() +bt.async_runtime.SubnetRegistrationRuntimeApi.dir() +bt.async_runtime.Commitments.dir() +bt.async_runtime.Commitments._metadata +bt.async_runtime.Commitments.LastCommitment._metadata +bt.async_runtime.Commitments +bt.async_runtime.Commitments.LastCommitment +bt.async_runtime.Commitments.MaxFields +'''.split('\n')] + +if len(sys.argv)>1: + block = int(sys.argv[1]) + bt.async_runtime.init(block=block) + +async def tests(calls): + # async_runtime requires explicit init call, or we'd have to await + # bt.async_runtime.SubtensorModule before we could use its properties. + await bt.async_runtime.init_metadata() + + # small demonstration of typical async usage with gather, matches first + # three test cases: + res3 = await asyncio.gather( + bt.async_runtime.SubtensorModule.NetworkLastRegistered(), + bt.async_runtime.SubtensorModule.NetworkLastLockCost(), + bt.async_runtime.SubtensorModule.NetworkImmunityPeriod() + ) + print('gather result:',res3) + + for call in calls: + if len(call) == 0 or call[0] == '#': + print(call) + continue + print(f'evaluating: {call}') + try: + if 'await' in call: + exec(f'async def fn():\n return {call}') + result = await locals()['fn']() + else: + result = eval(call) + print(f'--> {result}') + except Exception as e: + print(f'--> exception: {e}') + print() + +asyncio.run(tests(calls)) diff --git a/bittensor/utils/test_runtime_browser.py b/bittensor/utils/test_runtime_browser.py new file mode 100755 index 0000000000..a94caa9b41 --- /dev/null +++ b/bittensor/utils/test_runtime_browser.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python + +# testing code for runtime browser + +import bittensor as bt +import sys + +calls = [l for l in ''' +# subnet registration info +bt.runtime.SubtensorModule.NetworkLastRegistered() +bt.runtime.SubtensorModule.NetworkLastLockCost() +bt.runtime.SubtensorModule.NetworkImmunityPeriod() +bt.runtime.SubnetRegistrationRuntimeApi.get_network_registration_cost() +bt.runtime.SubtensorModule.SubnetOwner[29] +bt.runtime.SubtensorModule.SubnetLocked[29] +bt.runtime.SubtensorModule.NetworkRegisteredAt[29] +bt.runtime.SubtensorModule.NetworkLastLockCost(block=bt.runtime.SubtensorModule.NetworkRegisteredAt[29]) +bt.runtime.SubtensorModule.NetworkLastLockCost(block=bt.runtime.SubtensorModule.NetworkRegisteredAt[29]-1) +bt.runtime.SubnetRegistrationRuntimeApi.get_network_registration_cost(block=bt.runtime.SubtensorModule.NetworkRegisteredAt[29]) +bt.runtime.SubnetRegistrationRuntimeApi.get_network_registration_cost(block=bt.runtime.SubtensorModule.NetworkRegisteredAt[29]-1) +# hotkey registration and commitment info +bt.runtime.SubtensorModule.NetworkRegistrationAllowed[29] +bt.runtime.SubtensorModule.Burn(9) +bt.runtime.SubtensorModule.Burn(29) +bt.runtime.SubtensorModule.BlockAtRegistration[29,5] +bt.runtime.SubtensorModule.AdjustmentInterval[29] +bt.runtime.SubtensorModule.LastAdjustmentBlock[29] +bt.runtime.SubtensorModule.LastAdjustmentBlock(29,block=6000000) +bt.runtime.SubtensorModule.ImmunityPeriod[29] +bt.runtime.Commitments.LastCommitment[3, '5HTRBzc7CZvASdVZc7Fwzqnp1jXeP9z3KpoRKDZWhYSAksKq'] +bt.runtime.Commitments.CommitmentOf[3, '5HTRBzc7CZvASdVZc7Fwzqnp1jXeP9z3KpoRKDZWhYSAksKq'] +bt.runtime.Commitments.MaxFields() +bt.runtime.SubtensorModule.Weights[0,1] +bt.runtime.SubtensorModule.Weights(0,1) +bt.runtime.SubtensorModule.Weights(0,1,block=6000000) +bt.runtime.SubtensorModule.TotalHotkeyAlpha['5HmmmmhaoDbmt4KcYmAXETvfGBwFmcNgVCYSBH5JZRv6wrFf',29] +bt.runtime.SubtensorModule.TotalHotkeyAlpha('5HmmmmhaoDbmt4KcYmAXETvfGBwFmcNgVCYSBH5JZRv6wrFf',29,block=6000000) +# various +bt.runtime.Timestamp.Now() +bt.runtime.Timestamp.Now(block=6000000) +bt.runtime.Timestamp.Now(block=5900000) +bt.runtime.SubtensorModule.Alpha['5HmmmmhaoDbmt4KcYmAXETvfGBwFmcNgVCYSBH5JZRv6wrFf','5HHHHHzgLnYRvnKkHd45cRUDMHXTSwx7MjUzxBrKbY4JfZWn',29]['bits']/(1<<64) +bt.runtime.SubtensorModule.Alpha['5HHHHHzgLnYRvnKkHd45cRUDMHXTSwx7MjUzxBrKbY4JfZWn','5HHHHHzgLnYRvnKkHd45cRUDMHXTSwx7MjUzxBrKbY4JfZWn',29]['bits']/(1<<64) +bt.runtime.System.Account['5HHHHHzgLnYRvnKkHd45cRUDMHXTSwx7MjUzxBrKbY4JfZWn'] +bt.runtime.SubtensorModule.Owner['5GuNCPiUQBVoXCJgrMeXmc7rVjwT5zUEK5RzjhGmVqbQA5me'] +bt.runtime.Proxy.Proxies['5Enrkn6rRLGz4tK3TWamnbKYECKpz3hffNk9RBaDC426jLgb'] +# init runtime interface with particular fixed block: +bt.runtime.System.Number() +bt.runtime(block=6000000).System.Number() +bt.runtime.System.Number() +bt.runtime(block=None).System.Number() +# init runtime interface with particular network: +bt.runtime(network='test').System.Number() + +# exceptions, containing usage help: +bt.runtime.NonExistantPallet[0] +bt.runtime.System.NonExistentItem[0] +bt.runtime.SubnetRegistrationRuntimeApi.no_such_call() +bt.runtime.Timestamp.Now[1] +bt.runtime.SubtensorModule.AdjustmentInterval[0,0] +bt.runtime.System.Account() +bt.runtime.System.Account[0] +bt.runtime.System.Account['abc'] +bt.runtime.System.Account['5HHHHHzgLnYRvnKkHd45cRUDMHXTSwx7MjUzxBrKbY4JfZWn',0] + +# exposed metadata, lists and __repr__ containing usage help: +bt.runtime +bt.runtime.dir() +bt.runtime.SubnetRegistrationRuntimeApi.dir() +bt.runtime.Commitments.dir() +bt.runtime.Commitments._metadata +bt.runtime.Commitments.LastCommitment._metadata +bt.runtime.Commitments +bt.runtime.Commitments.LastCommitment +bt.runtime.Commitments.MaxFields +'''.split('\n')] + +if len(sys.argv)>1: + block = int(sys.argv[1]) + bt.runtime.init(block=block) + +for call in calls: + if len(call) == 0 or call[0] == '#': + print(call) + continue + print(f'evaluating: {call}') + try: + result = eval(call) + print(f'--> {result}') + except Exception as e: + print(f'--> exception: {e}') + print() diff --git a/bittensor/utils/version.py b/bittensor/utils/version.py new file mode 100644 index 0000000000..c8f899d760 --- /dev/null +++ b/bittensor/utils/version.py @@ -0,0 +1,142 @@ +import time +from pathlib import Path +from typing import Optional + +import requests +from packaging.version import Version, InvalidVersion + +from bittensor import __name__ +from bittensor.core.settings import __version__, PIPADDRESS +from bittensor.utils.btlogging import logging + +VERSION_CHECK_THRESHOLD = 86400 + + +class VersionCheckError(Exception): + """Exception raised for errors in the version check process.""" + + +def _get_version_file_path() -> Path: + return Path.home() / ".bittensor" / ".last_known_version" + + +def _get_version_from_file(version_file: Path) -> Optional[str]: + try: + mtime = version_file.stat().st_mtime + logging.debug(f"Found version file, last modified: {mtime}") + diff = time.time() - mtime + + if diff >= VERSION_CHECK_THRESHOLD: + logging.debug("Version file expired") + return None + + return version_file.read_text() + except FileNotFoundError: + logging.debug("No bittensor version file found") + return None + except OSError: + logging.exception("Failed to read version file") + return None + + +def _get_version_from_pypi(timeout: int = 15) -> str: + logging.debug(f"Checking latest Bittensor version at: {PIPADDRESS}") + try: + response = requests.get(PIPADDRESS, timeout=timeout) + latest_version = response.json()["info"]["version"] + return latest_version + except requests.exceptions.RequestException: + logging.exception("Failed to get latest version from pypi") + raise + + +def get_and_save_latest_version(timeout: int = 15) -> str: + """ + Retrieves and saves the latest version of Bittensor. + + Args: + timeout (int): The timeout for the request to PyPI in seconds. Default is ``15``. + + Returns: + str: The latest version of Bittensor. + """ + version_file = _get_version_file_path() + + if last_known_version := _get_version_from_file(version_file): + return last_known_version + + latest_version = _get_version_from_pypi(timeout) + + try: + version_file.write_text(latest_version) + except OSError: + logging.exception("Failed to save latest version to file") + + return latest_version + + +def check_version(timeout: int = 15): + """ + Check if the current version of Bittensor is up-to-date with the latest version on PyPi. + Raises a VersionCheckError if the version check fails. + + Args: + timeout (int): The timeout for the request to PyPI in seconds. Default is ``15``. + """ + + try: + latest_version = get_and_save_latest_version(timeout) + + if Version(latest_version) > Version(__version__): + print( + f"\u001b[33mBittensor Version: Current {__version__}/Latest {latest_version}\n" + f"Please update to the latest version at your earliest convenience. " + "Run the following command to upgrade:\n\n\u001b[0mpython -m pip install --upgrade bittensor" + ) + pass + except Exception as e: + raise VersionCheckError("Version check failed") from e + + +def version_checking(timeout: int = 15): + """Deprecated, kept for backwards compatibility. Use check_version() instead. + + Args: + timeout (int): The timeout for calling :func:``check_version`` function. Default is ``15``. + """ + + from warnings import warn + + warn( + "version_checking() is deprecated, please use check_version() instead", + DeprecationWarning, + ) + + try: + check_version(timeout) + except VersionCheckError: + logging.exception("Version check failed") + + +def check_latest_version_in_pypi(): + """Check for the latest version of the package on PyPI.""" + package_name = __name__ + url = f"https://pypi.org/pypi/{package_name}/json" + + try: + response = requests.get(url, timeout=5) + response.raise_for_status() + latest_version = response.json()["info"]["version"] + installed_version = __version__ + try: + if Version(installed_version) < Version(latest_version): + print( + f"\n🔔 New version is available `{package_name} v.{latest_version}`" + ) + print("📦 Use command `pip install --upgrade bittensor` to update.") + except InvalidVersion: + # stay silent if InvalidVersion + pass + except (requests.RequestException, KeyError) as e: + # stay silent if not internet connection or pypi.org issue + pass diff --git a/bittensor/utils/weight_utils.py b/bittensor/utils/weight_utils.py new file mode 100644 index 0000000000..2a7b7f3afd --- /dev/null +++ b/bittensor/utils/weight_utils.py @@ -0,0 +1,481 @@ +"""Conversion for weight between chain representation and np.array or torch.Tensor""" + +import hashlib +import typing +from typing import Union, Optional + +import numpy as np +from bittensor_wallet import Keypair +from numpy.typing import NDArray +from scalecodec import U16, ScaleBytes, Vec + +from bittensor.utils.btlogging import logging +from bittensor.utils.registration import legacy_torch_api_compat, torch, use_torch + +if typing.TYPE_CHECKING: + from bittensor.core.metagraph import Metagraph + from bittensor.core.subtensor import Subtensor + + +U32_MAX = 4294967295 +U16_MAX = 65535 + + +# Uses in `bittensor.utils.weight_utils.process_weights_for_netuid` +@legacy_torch_api_compat +def normalize_max_weight( + x: Union[NDArray[np.float32], "torch.FloatTensor"], limit: float = 0.1 +) -> Union[NDArray[np.float32], "torch.FloatTensor"]: + """Normalizes the tensor x so that sum(x) = 1 and the max value is not greater than the limit. + Args: + x (:obj:`np.float32`): Tensor to be max_value normalized. + limit: float: Max value after normalization. + + Returns: + y (:obj:`np.float32`): Normalized x tensor. + """ + epsilon = 1e-7 # For numerical stability after normalization + + weights = x.copy() + values = np.sort(weights) + + if x.sum() == 0 or x.shape[0] * limit <= 1: + return np.ones_like(x) / x.shape[0] + else: + estimation = values / values.sum() + + if estimation.max() <= limit: + return weights / weights.sum() + + # Find the cumulative sum and sorted tensor + cumsum = np.cumsum(estimation, 0) + + # Determine the index of cutoff + estimation_sum = np.array( + [(len(values) - i - 1) * estimation[i] for i in range(len(values))] + ) + n_values = (estimation / (estimation_sum + cumsum + epsilon) < limit).sum() + + # Determine the cutoff based on the index + cutoff_scale = (limit * cumsum[n_values - 1] - epsilon) / ( + 1 - (limit * (len(estimation) - n_values)) + ) + cutoff = cutoff_scale * values.sum() + + # Applying the cutoff + weights[weights > cutoff] = cutoff + + y = weights / weights.sum() + + return y + + +# Metagraph uses this function. +def convert_weight_uids_and_vals_to_tensor( + n: int, uids: list[int], weights: list[int] +) -> Union[NDArray[np.float32], "torch.FloatTensor"]: + """ + Converts weights and uids from chain representation into a np.array (inverse operation from + convert_weights_and_uids_for_emit). + + Args: + n (int): number of neurons on network. + uids (list[int]): Tensor of uids as destinations for passed weights. + weights (list[int]): Tensor of weights. + + Returns: + row_weights (np.float32 or torch.FloatTensor): Converted row weights. + """ + row_weights = ( + torch.zeros([n], dtype=torch.float32) + if use_torch() + else np.zeros([n], dtype=np.float32) + ) + for uid_j, wij in list(zip(uids, weights)): + row_weights[uid_j] = float( + wij + ) # assumes max-upscaled values (w_max = U16_MAX). + row_sum = row_weights.sum() + if row_sum > 0: + row_weights /= row_sum # normalize + return row_weights + + +# Metagraph uses this function. +def convert_root_weight_uids_and_vals_to_tensor( + n: int, uids: list[int], weights: list[int], subnets: list[int] +) -> Union[NDArray[np.float32], "torch.FloatTensor"]: + """Converts root weights and uids from chain representation into a np.array or torch FloatTensor + (inverse operation from convert_weights_and_uids_for_emit) + + Args: + n (int): number of neurons on network. + uids (list[int]): Tensor of uids as destinations for passed weights. + weights (list[int]): Tensor of weights. + subnets (list[int]): list of subnets on the network. + + Returns: + row_weights (np.float32): Converted row weights. + """ + row_weights = ( + torch.zeros([n], dtype=torch.float32) + if use_torch() + else np.zeros([n], dtype=np.float32) + ) + for uid_j, wij in list(zip(uids, weights)): + if uid_j in subnets: + index_s = subnets.index(uid_j) + row_weights[index_s] = float( + wij + ) # assumes max-upscaled values (w_max = U16_MAX). + else: + logging.warning( + f"Incorrect Subnet uid {uid_j} in Subnets {subnets}. The subnet is unavailable at the moment." + ) + continue + row_sum = row_weights.sum() + if row_sum > 0: + row_weights /= row_sum # normalize + return row_weights + + +# Metagraph uses this function. +def convert_bond_uids_and_vals_to_tensor( + n: int, uids: list[int], bonds: list[int] +) -> Union[NDArray[np.int64], "torch.LongTensor"]: + """Converts bond and uids from chain representation into a np.array. + + Args: + n (int): number of neurons on network. + uids (list[int]): Tensor of uids as destinations for passed bonds. + bonds (list[int]): Tensor of bonds. + + Returns: + row_bonds (np.float32): Converted row bonds. + """ + row_bonds = ( + torch.zeros([n], dtype=torch.int64) + if use_torch() + else np.zeros([n], dtype=np.int64) + ) + for uid_j, bij in list(zip(uids, bonds)): + row_bonds[uid_j] = int(bij) + return row_bonds + + +# This is used by the community via `bittensor.api.extrinsics.set_weights.set_weights_extrinsic` +def convert_weights_and_uids_for_emit( + uids: Union[NDArray[np.int64], "torch.LongTensor"], + weights: Union[NDArray[np.float32], "torch.FloatTensor"], +) -> tuple[list[int], list[int]]: + """Converts weights into integer u32 representation that sum to MAX_INT_WEIGHT. + + Args: + uids (np.int64):Tensor of uids as destinations for passed weights. + weights (np.float32):Tensor of weights. + + Returns: + weight_uids (list[int]): Uids as a list. + weight_vals (list[int]): Weights as a list. + """ + # Checks. + weights = weights.tolist() + uids = uids.tolist() + if min(weights) < 0: + raise ValueError(f"Passed weight is negative cannot exist on chain {weights}") + if min(uids) < 0: + raise ValueError(f"Passed uid is negative cannot exist on chain {uids}") + if len(uids) != len(weights): + raise ValueError( + f"Passed weights and uids must have the same length, got {len(uids)} and {len(weights)}" + ) + if sum(weights) == 0: + return [], [] # Nothing to set on chain. + else: + max_weight = float(max(weights)) + weights = [ + float(value) / max_weight for value in weights + ] # max-upscale values (max_weight = 1). + + weight_vals = [] + weight_uids = [] + for i, (weight_i, uid_i) in enumerate(list(zip(weights, uids))): + uint16_val = round( + float(weight_i) * int(U16_MAX) + ) # convert to int representation. + + # Filter zeros + if uint16_val != 0: # Filter zeros + weight_vals.append(uint16_val) + weight_uids.append(uid_i) + + return weight_uids, weight_vals + + +# The community uses / bittensor does not +def process_weights_for_netuid( + uids: Union[NDArray[np.int64], "torch.Tensor"], + weights: Union[NDArray[np.float32], "torch.Tensor"], + netuid: int, + subtensor: "Subtensor", + metagraph: Optional["Metagraph"] = None, + exclude_quantile: int = 0, +) -> Union[ + tuple["torch.Tensor", "torch.FloatTensor"], + tuple[NDArray[np.int64], NDArray[np.float32]], +]: + """ + Processes weight tensors for a given subnet id using the provided weight and UID arrays, applying constraints + and normalization based on the subtensor and metagraph data. This function can handle both NumPy arrays and PyTorch + tensors. + + Args: + uids (Union[NDArray[np.int64], "torch.Tensor"]): Array of unique identifiers of the neurons. + weights (Union[NDArray[np.float32], "torch.Tensor"]): Array of weights associated with the user IDs. + netuid (int): The network uid to process weights for. + subtensor (Subtensor): Subtensor instance to access blockchain data. + metagraph (Optional[Metagraph]): Metagraph instance for additional network data. If None, it is fetched from + the subtensor using the netuid. + exclude_quantile (int): Quantile threshold for excluding lower weights. Defaults to ``0``. + + Returns: + Union[tuple["torch.Tensor", "torch.FloatTensor"], tuple[NDArray[np.int64], NDArray[np.float32]]]: tuple + containing the array of user IDs and the corresponding normalized weights. The data type of the return + matches the type of the input weights (NumPy or PyTorch). + """ + + logging.debug("process_weights_for_netuid()") + logging.debug(f"netuid {netuid}") + logging.debug(f"subtensor: {subtensor}") + logging.debug(f"metagraph: {metagraph}") + + # Get latest metagraph from chain if metagraph is None. + if metagraph is None: + metagraph = subtensor.metagraph(netuid) + + return process_weights( + uids=uids, + weights=weights, + num_neurons=metagraph.n, + min_allowed_weights=subtensor.min_allowed_weights(netuid=netuid), + max_weight_limit=subtensor.max_weight_limit(netuid=netuid), + exclude_quantile=exclude_quantile, + ) + + +def process_weights( + uids: Union[NDArray[np.int64], "torch.Tensor"], + weights: Union[NDArray[np.float32], "torch.Tensor"], + num_neurons: int, + min_allowed_weights: Optional[int], + max_weight_limit: Optional[float], + exclude_quantile: int = 0, +) -> Union[ + tuple["torch.Tensor", "torch.FloatTensor"], + tuple[NDArray[np.int64], NDArray[np.float32]], +]: + """ + Processes weight tensors for a given weights and UID arrays and hyperparams, applying constraints + and normalization based on the subtensor and metagraph data. This function can handle both NumPy arrays and PyTorch + tensors. + + Args: + uids (Union[NDArray[np.int64], "torch.Tensor"]): Array of unique identifiers of the neurons. + weights (Union[NDArray[np.float32], "torch.Tensor"]): Array of weights associated with the user IDs. + num_neurons (int): The number of neurons in the network. + min_allowed_weights (Optional[int]): Subnet hyperparam Minimum number of allowed weights. + max_weight_limit (Optional[float]): Subnet hyperparam Maximum weight limit. + exclude_quantile (int): Quantile threshold for excluding lower weights. Defaults to ``0``. + + Returns: + Union[tuple["torch.Tensor", "torch.FloatTensor"], tuple[NDArray[np.int64], NDArray[np.float32]]]: tuple + containing the array of user IDs and the corresponding normalized weights. The data type of the return + matches the type of the input weights (NumPy or PyTorch). + """ + logging.debug("process_weights()") + logging.debug(f"weights: {weights}") + + # Cast weights to floats. + if use_torch(): + if not isinstance(weights, torch.FloatTensor): + weights = weights.type(torch.float32) + else: + if not isinstance(weights, np.float32): + weights = weights.astype(np.float32) + + # Network configuration parameters from an subtensor. + # These parameters determine the range of acceptable weights for each neuron. + quantile = exclude_quantile / U16_MAX + logging.debug(f"quantile: {quantile}") + logging.debug(f"min_allowed_weights: {min_allowed_weights}") + logging.debug(f"max_weight_limit: {max_weight_limit}") + + # Find all non zero weights. + non_zero_weight_idx = ( + torch.argwhere(weights > 0).squeeze(dim=1) + if use_torch() + else np.argwhere(weights > 0).squeeze(axis=1) + ) + non_zero_weight_uids = uids[non_zero_weight_idx] + non_zero_weights = weights[non_zero_weight_idx] + nzw_size = non_zero_weights.numel() if use_torch() else non_zero_weights.size + if nzw_size == 0 or num_neurons < min_allowed_weights: + logging.warning("No non-zero weights returning all ones.") + final_weights = ( + torch.ones(num_neurons).to(num_neurons) / num_neurons + if use_torch() + else np.ones(num_neurons, dtype=np.int64) / num_neurons + ) + logging.debug(f"final_weights: {final_weights}") + final_weights_count = ( + torch.tensor(list(range(len(final_weights)))) + if use_torch() + else np.arange(len(final_weights)) + ) + return ( + (final_weights_count, final_weights) + if use_torch() + else (final_weights_count, final_weights) + ) + + elif nzw_size < min_allowed_weights: + logging.warning( + "No non-zero weights less then min allowed weight, returning all ones." + ) + # ( const ): Should this be np.zeros( ( num_neurons ) ) to reset everyone to build up weight? + weights = ( + torch.ones(num_neurons).to(num_neurons) * 1e-5 + if use_torch() + else np.ones(num_neurons, dtype=np.int64) * 1e-5 + ) # creating minimum even non-zero weights + weights[non_zero_weight_idx] += non_zero_weights + logging.debug(f"final_weights: {weights}") + normalized_weights = normalize_max_weight(x=weights, limit=max_weight_limit) + nw_arange = ( + torch.tensor(list(range(len(normalized_weights)))) + if use_torch() + else np.arange(len(normalized_weights)) + ) + return nw_arange, normalized_weights + + logging.debug(f"non_zero_weights: {non_zero_weights}") + + # Compute the exclude quantile and find the weights in the lowest quantile + max_exclude = max(0, len(non_zero_weights) - min_allowed_weights) / len( + non_zero_weights + ) + exclude_quantile = min([quantile, max_exclude]) + lowest_quantile = ( + non_zero_weights.quantile(exclude_quantile) + if use_torch() + else np.quantile(non_zero_weights, exclude_quantile) + ) + logging.debug(f"max_exclude: {max_exclude}") + logging.debug(f"exclude_quantile: {exclude_quantile}") + logging.debug(f"lowest_quantile: {lowest_quantile}") + + # Exclude all weights below the allowed quantile. + non_zero_weight_uids = non_zero_weight_uids[lowest_quantile <= non_zero_weights] + non_zero_weights = non_zero_weights[lowest_quantile <= non_zero_weights] + logging.debug(f"non_zero_weight_uids: {non_zero_weight_uids}") + logging.debug(f"non_zero_weights: {non_zero_weights}") + + # Normalize weights and return. + normalized_weights = normalize_max_weight( + x=non_zero_weights, limit=max_weight_limit + ) + logging.debug(f"final_weights: {normalized_weights}") + + return non_zero_weight_uids, normalized_weights + + +def generate_weight_hash( + address: str, + netuid: int, + uids: list[int], + values: list[int], + version_key: int, + salt: list[int], +) -> str: + """ + Generate a valid commit hash from the provided weights. + + Args: + address (str): The account identifier. Wallet ss58_address. + netuid (int): The network unique identifier. + uids (list[int]): The list of UIDs. + salt (list[int]): The salt to add to hash. + values (list[int]): The list of weight values. + version_key (int): The version key. + + Returns: + str: The generated commit hash. + """ + # Encode data using SCALE codec + wallet_address = ScaleBytes(Keypair(ss58_address=address).public_key) + netuid = ScaleBytes(netuid.to_bytes(2, "little")) + + vec_uids = Vec(data=None, sub_type="U16") + vec_uids.value = [U16(ScaleBytes(uid.to_bytes(2, "little"))) for uid in uids] + uids = ScaleBytes(vec_uids.encode().data) + + vec_values = Vec(data=None, sub_type="U16") + vec_values.value = [ + U16(ScaleBytes(value.to_bytes(2, "little"))) for value in values + ] + values = ScaleBytes(vec_values.encode().data) + + version_key = ScaleBytes(version_key.to_bytes(8, "little")) + + vec_salt = Vec(data=None, sub_type="U16") + vec_salt.value = [U16(ScaleBytes(salts.to_bytes(2, "little"))) for salts in salt] + salt = ScaleBytes(vec_salt.encode().data) + + data = wallet_address + netuid + uids + values + salt + version_key + + # Generate Blake2b hash of the data tuple + blake2b_hash = hashlib.blake2b(data.data, digest_size=32) + + # Convert the hash to hex string and add "0x" prefix + commit_hash = "0x" + blake2b_hash.hexdigest() + + return commit_hash + + +def convert_uids_and_weights( + uids: Union[NDArray[np.int64], list], + weights: Union[NDArray[np.float32], list], +) -> tuple[np.ndarray, np.ndarray]: + """Converts netuids and weights to numpy arrays if they are not already. + + Arguments: + uids (Union[NDArray[np.int64], list]): The uint64 uids of destination neurons. + weights (Union[NDArray[np.float32], list]): The weights to set. These must be floated. + + Returns: + tuple[ndarray, ndarray]: Bytes converted netuids and weights. + """ + if isinstance(uids, list): + uids = np.array(uids, dtype=np.int64) + if isinstance(weights, list): + weights = np.array(weights, dtype=np.float32) + return uids, weights + + +def convert_and_normalize_weights_and_uids( + uids: Union[NDArray[np.int64], "torch.LongTensor", list], + weights: Union[NDArray[np.float32], "torch.FloatTensor", list], +) -> tuple[list[int], list[int]]: + """Converts weights and uids to numpy arrays if they are not already. + + Arguments: + uids (Union[NDArray[np.int64], torch.LongTensor, list]): The ``uint64`` uids of destination neurons. + weights (Union[NDArray[np.float32], torch.FloatTensor, list]): The weights to set. These must be ``float`` s + and correspond to the passed ``uid`` s. + + Returns: + weight_uids, weight_vals: Bytes converted weights and uids + """ + + # Reformat and normalize and return + return convert_weights_and_uids_for_emit(*convert_uids_and_weights(uids, weights)) diff --git a/build.sh b/build.sh deleted file mode 100755 index dd884ade03..0000000000 --- a/build.sh +++ /dev/null @@ -1 +0,0 @@ -python3 -m grpc.tools.protoc bittensor/bittensor.proto -I. --python_out=. --grpc_python_out=. diff --git a/contrib/CODE_REVIEW_DOCS.md b/contrib/CODE_REVIEW_DOCS.md new file mode 100644 index 0000000000..9909606a89 --- /dev/null +++ b/contrib/CODE_REVIEW_DOCS.md @@ -0,0 +1,72 @@ +# Code Review +### Conceptual Review + +A review can be a conceptual review, where the reviewer leaves a comment + * `Concept (N)ACK`, meaning "I do (not) agree with the general goal of this pull + request", + * `Approach (N)ACK`, meaning `Concept ACK`, but "I do (not) agree with the + approach of this change". + +A `NACK` needs to include a rationale why the change is not worthwhile. +NACKs without accompanying reasoning may be disregarded. +After conceptual agreement on the change, code review can be provided. A review +begins with `ACK BRANCH_COMMIT`, where `BRANCH_COMMIT` is the top of the PR +branch, followed by a description of how the reviewer did the review. The +following language is used within pull request comments: + + - "I have tested the code", involving change-specific manual testing in + addition to running the unit, functional, or fuzz tests, and in case it is + not obvious how the manual testing was done, it should be described; + - "I have not tested the code, but I have reviewed it and it looks + OK, I agree it can be merged"; + - A "nit" refers to a trivial, often non-blocking issue. + +### Code Review +Project maintainers reserve the right to weigh the opinions of peer reviewers +using common sense judgement and may also weigh based on merit. Reviewers that +have demonstrated a deeper commitment and understanding of the project over time +or who have clear domain expertise may naturally have more weight, as one would +expect in all walks of life. + +Where a patch set affects consensus-critical code, the bar will be much +higher in terms of discussion and peer review requirements, keeping in mind that +mistakes could be very costly to the wider community. This includes refactoring +of consensus-critical code. + +Where a patch set proposes to change the Bittensor consensus, it must have been +discussed extensively on the discord server and other channels, be accompanied by a widely +discussed BIP and have a generally widely perceived technical consensus of being +a worthwhile change based on the judgement of the maintainers. + +### Finding Reviewers + +As most reviewers are themselves developers with their own projects, the review +process can be quite lengthy, and some amount of patience is required. If you find +that you've been waiting for a pull request to be given attention for several +months, there may be a number of reasons for this, some of which you can do something +about: + + - It may be because of a feature freeze due to an upcoming release. During this time, + only bug fixes are taken into consideration. If your pull request is a new feature, + it will not be prioritized until after the release. Wait for the release. + - It may be because the changes you are suggesting do not appeal to people. Rather than + nits and critique, which require effort and means they care enough to spend time on your + contribution, thundering silence is a good sign of widespread (mild) dislike of a given change + (because people don't assume *others* won't actually like the proposal). Don't take + that personally, though! Instead, take another critical look at what you are suggesting + and see if it: changes too much, is too broad, doesn't adhere to the + [developer notes](DEVELOPMENT_WORKFLOW.md), is dangerous or insecure, is messily written, etc. + Identify and address any of the issues you find. Then ask e.g. on IRC if someone could give + their opinion on the concept itself. + - It may be because your code is too complex for all but a few people, and those people + may not have realized your pull request even exists. A great way to find people who + are qualified and care about the code you are touching is the + [Git Blame feature](https://docs.github.com/en/github/managing-files-in-a-repository/managing-files-on-github/tracking-changes-in-a-file). Simply + look up who last modified the code you are changing and see if you can find + them and give them a nudge. Don't be incessant about the nudging, though. + - Finally, if all else fails, ask on IRC or elsewhere for someone to give your pull request + a look. If you think you've been waiting for an unreasonably long time (say, + more than a month) for no particular reason (a few lines changed, etc.), + this is totally fine. Try to return the favor when someone else is asking + for feedback on their code, and the universe balances out. + - Remember that the best thing you can do while waiting is give review to others! \ No newline at end of file diff --git a/contrib/CONTRIBUTING.md b/contrib/CONTRIBUTING.md new file mode 100644 index 0000000000..c3d6ba850f --- /dev/null +++ b/contrib/CONTRIBUTING.md @@ -0,0 +1,299 @@ +# Contributing to Bittensor + +The following is a set of guidelines for contributing to Bittensor, which are hosted in the [Opentensor Organization](https://github.com/opentensor) on GitHub. These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request. + +## Table Of Contents +1. [I don't want to read this whole thing, I just have a question!!!](#i-dont-want-to-read-this-whole-thing-i-just-have-a-question) +1. [What should I know before I get started?](#what-should-i-know-before-i-get-started) +1. [Getting Started](#getting-started) + 1. [Good First Issue Label](#good-first-issue-label) + 1. [Beginner and Help-wanted Issues Label](#beginner-and-help-wanted-issues-label) +1. [How Can I Contribute?](#how-can-i-contribute) + 1. [Code Contribution General Guideline](#code-contribution-general-guidelines) + 1. [Pull Request Philosophy](#pull-request-philosophy) + 1. [Pull Request Process](#pull-request-process) + 1. [Testing](#testing) + 1. [Addressing Feedback](#addressing-feedback) + 1. [Squashing Commits](#squashing-commits) + 1. [Refactoring](#refactoring) + 1. [Peer Review](#peer-review) + 1. [Reporting Bugs](#reporting-bugs) + 1. [Suggesting Features](#suggesting-enhancements-and-features) + + +## I don't want to read this whole thing I just have a question! + +> **Note:** Please don't file an issue to ask a question. You'll get faster results by using the resources below. + +We have an official Discord server where the community chimes in with helpful advice if you have questions. +This is the fastest way to get an answer and the core development team is active on Discord. + +* [Official Bittensor Discord](https://discord.gg/7wvFuPJZgq) + +## What should I know before I get started? +Bittensor is still in the Alpha stages, and as such you will likely run into some problems in deploying your model or installing Bittensor itself. If you run into an issue or end up resolving an issue yourself, feel free to create a pull request with a fix or with a fix to the documentation. The documentation repository can be found [here](https://github.com/opentensor/docs). + +Additionally, note that the core implementation of Bittensor consists of two separate repositories: [The core Bittensor code](https://github.com/opentensor/bittensor) and the Bittensor Blockchain [subtensor](https://github.com/opentensor/subtensor). + +Supplemental repository for the Bittensor subnet template can be found [here](https://github.com/opentensor/bittensor-subnet-template). This is a great first place to look for getting your hands dirty and start learning and building on Bittensor. See the subnet links [page](https://github.com/opentensor/bittensor-subnet-template/blob/main/subnet_links.json) for a list of all the repositories for the active registered subnets. + +## Getting Started +New contributors are very welcome and needed. +Reviewing and testing is highly valued and the most effective way you can contribute as a new contributor. It also will teach you much more about the code and process than opening pull requests. + +Before you start contributing, familiarize yourself with the Bittensor Core build system and tests. Refer to the documentation in the repository on how to build Bittensor core and how to run the unit tests, functional tests. + +There are many open issues of varying difficulty waiting to be fixed. If you're looking for somewhere to start contributing, check out the [good first issue](https://github.com/opentensor/bittensor/labels/good%20first%20issue) list or changes that are up for grabs. Some of them might no longer be applicable. So if you are interested, but unsure, you might want to leave a comment on the issue first. Also peruse the [issues](https://github.com/opentensor/bittensor/issues) tab for all open issues. + +### Good First Issue Label +The purpose of the good first issue label is to highlight which issues are suitable for a new contributor without a deep understanding of the codebase. + +However, good first issues can be solved by anyone. If they remain unsolved for a longer time, a frequent contributor might address them. + +You do not need to request permission to start working on an issue. However, you are encouraged to leave a comment if you are planning to work on it. This will help other contributors monitor which issues are actively being addressed and is also an effective way to request assistance if and when you need it. + +### Beginner and Help-wanted Issues Label +You can start by looking through these `beginner` and `help-wanted` issues: + +* [Beginner issues](https://github.com/opentensor/bittensor/labels/beginner) - issues which should only require a few lines of code, and a test or two. +* [Help wanted issues](https://github.com/opentensor/bittensor/labels/help%20wanted) - issues which should be a bit more involved than `beginner` issues. + +## Communication Channels +Most communication about Bittensor development happens on Discord channel. +Here's the link of Discord community. +[Bittensor Discord](https://discord.com/channels/799672011265015819/799672011814862902) + +And also here. +[Bittensor Community Discord](https://discord.com/channels/1120750674595024897/1120799375703162950) + +## How Can I Contribute? + +You can contribute to Bittensor in one of two main ways (as well as many others): +1. [Bug](#reporting-bugs) reporting and fixes +2. New features and Bittensor [enhancements](#suggesting-enhancements-and-features) + +> Please follow the Bittensor [style guide](./STYLE.md) regardless of your contribution type. + +Here is a high-level summary: +- Code consistency is crucial; adhere to established programming language conventions. +- Use `ruff format .` to format your Python code; it ensures readability and consistency. +- Write concise Git commit messages; summarize changes in ~50 characters. +- Follow these six commit rules: + - Atomic Commits: Focus on one task or fix per commit. + - Subject and Body Separation: Use a blank line to separate the subject from the body. + - Subject Line Length: Keep it under 50 characters for readability. + - Imperative Mood: Write subject line as if giving a command or instruction. + - Body Text Width: Wrap text manually at 72 characters. + - Body Content: Explain what changed and why, not how. +- Make use of your commit messages to simplify project understanding and maintenance. + +> For clear examples of each of the commit rules, see the style guide's [rules](./STYLE.md#the-six-rules-of-a-great-commit) section. + +### Code Contribution General Guidelines + +> Review the Bittensor [style guide](./STYLE.md) and [development workflow](./DEVELOPMENT_WORKFLOW.md) before contributing. + +If you're looking to contribute to Bittensor but unsure where to start, please join our community [discord](https://discord.gg/bittensor), a developer-friendly Bittensor town square. Start with [#development](https://discord.com/channels/799672011265015819/799678806159392768) and [#bounties](https://discord.com/channels/799672011265015819/1095684873810890883) to see what issues are currently posted. For a greater understanding of Bittensor's usage and development, check the [Bittensor Documentation](https://docs.bittensor.com). + +#### Pull Request Philosophy + +Patchsets and enhancements should always be focused. A pull request could add a feature, fix a bug, or refactor code, but it should not contain a mixture of these. Please also avoid 'super' pull requests which attempt to do too much, are overly large, or overly complex as this makes review difficult. + +Specifically, pull requests must adhere to the following criteria: +- **Must** branch off from `staging`. Make sure that all your PRs are using `staging` branch as a base or will be closed. +- Contain fewer than 50 files. PRs with more than 50 files will be closed. +- Use the specific [template](../.github/pull_request_template.md) appropriate to your contribution. +- If a PR introduces a new feature, it *must* include corresponding tests. +- Other PRs (bug fixes, refactoring, etc.) should ideally also have tests, as they provide proof of concept and prevent regression. +- Categorize your PR properly by using GitHub labels. This aids in the review process by informing reviewers about the type of change at a glance. +- Make sure your code includes adequate comments. These should explain why certain decisions were made and how your changes work. +- If your changes are extensive, consider breaking your PR into smaller, related PRs. This makes your contributions easier to understand and review. +- Be active in the discussion about your PR. Respond promptly to comments and questions to help reviewers understand your changes and speed up the acceptance process. + +Generally, all pull requests must: + + - Have a clear use case, fix a demonstrable bug or serve the greater good of the project (e.g. refactoring for modularisation). + - Be well peer-reviewed. + - Follow code style guidelines. + - Not break the existing test suite. + - Where bugs are fixed, where possible, there should be unit tests demonstrating the bug and also proving the fix. + - Change relevant comments and documentation when behaviour of code changes. + +#### Pull Request Process + +Please follow these steps to have your contribution considered by the maintainers: + +*Before* creating the PR: +1. Read the [development workflow](./DEVELOPMENT_WORKFLOW.md) defined for this repository to understand our workflow. +2. Ensure your PR meets the criteria stated in the 'Pull Request Philosophy' section. +3. Include relevant tests for any fixed bugs or new features as stated in the [testing guide](./TESTING.md). +4. Follow all instructions in [the template](../.github/pull_request_template.md) to create the PR. +5. Ensure your commit messages are clear and concise. Include the issue number if applicable. +6. If you have multiple commits, rebase them into a single commit using `git rebase -i`. +7. Explain what your changes do and why you think they should be merged in the PR description consistent with the [style guide](./STYLE.md). + +*After* creating the PR: +1. Verify that all [status checks](https://help.github.com/articles/about-status-checks/) are passing after you submit your pull request. +2. Label your PR using GitHub's labeling feature. The labels help categorize the PR and streamline the review process. +3. Document your code with comments that provide a clear understanding of your changes. Explain any non-obvious parts of your code or design decisions you've made. +4. If your PR has extensive changes, consider splitting it into smaller, related PRs. This reduces the cognitive load on the reviewers and speeds up the review process. + +Please be responsive and participate in the discussion on your PR! This aids in clarifying any confusion or concerns and leads to quicker resolution and merging of your PR. + +> Note: If your changes are not ready for merge but you want feedback, create a draft pull request. + +Following these criteria will aid in quicker review and potential merging of your PR. +While the prerequisites above must be satisfied prior to having your pull request reviewed, the reviewer(s) may ask you to complete additional design work, tests, or other changes before your pull request can be ultimately accepted. + +When you are ready to submit your changes, create a pull request: + +> **Always** follow the [style guide](./STYLE.md) and [development workflow](./DEVELOPMENT_WORKFLOW.md) before submitting pull requests. + +After you submit a pull request, it will be reviewed by the maintainers. They may ask you to make changes. Please respond to any comments and push your changes as a new commit. + +> Note: Be sure to merge the latest from "upstream" before making a pull request: + +```bash +git remote add upstream https://github.com/opentensor/bittensor.git +git fetch upstream +git merge upstream/ +git push origin +``` + +#### Testing +Before making a PR for any code changes, please write adequate testing with unittest and/or pytest if it is warranted. This is **mandatory** for new features and enhancements. See the [testing guide](./TESTING.md) for more complete information. + +You may also like to view the [/tests](https://github.com/opentensor/bittensor/tree/master/tests) for starter examples. + +Here is a quick summary: +- **Running Tests**: Use `pytest` from the root directory of the Bittensor repository to run all tests. To run a specific test file or a specific test within a file, specify it directly (e.g., `pytest tests/test_wallet.py::test_create_new_coldkey`). +- **Writing Tests**: When writing tests, cover both the "happy path" and any potential error conditions. Use the `assert` statement to verify the expected behavior of a function. +- **Mocking**: Use the `unittest.mock` library to mock certain functions or objects when you need to isolate the functionality you're testing. This allows you to control the behavior of these functions or objects during testing. +- **Test Coverage**: Use the `pytest-cov` plugin to measure your test coverage. Aim for high coverage but also ensure your tests are meaningful and accurately represent the conditions under which your code will run. +- **Continuous Integration**: Bittensor uses GitHub Actions for continuous integration. Tests are automatically run every time you push changes to the repository. Check the "Actions" tab of the Bittensor GitHub repository to view the results. + +Remember, testing is crucial for maintaining code health, catching issues early, and facilitating the addition of new features or refactoring of existing code. + +#### Addressing Feedback + +After submitting your pull request, expect comments and reviews from other contributors. You can add more commits to your pull request by committing them locally and pushing to your fork. + +You are expected to reply to any review comments before your pull request is merged. You may update the code or reject the feedback if you do not agree with it, but you should express so in a reply. If there is outstanding feedback and you are not actively working on it, your pull request may be closed. + +#### Squashing Commits + +If your pull request contains fixup commits (commits that change the same line of code repeatedly) or too fine-grained commits, you may be asked to [squash](https://git-scm.com/docs/git-rebase#_interactive_mode) your commits before it will be reviewed. The basic squashing workflow is shown below. + + git checkout your_branch_name + git rebase -i HEAD~n + # n is normally the number of commits in the pull request. + # Set commits (except the one in the first line) from 'pick' to 'squash', save and quit. + # On the next screen, edit/refine commit messages. + # Save and quit. + git push -f # (force push to GitHub) + +Please update the resulting commit message, if needed. It should read as a coherent message. In most cases, this means not just listing the interim commits. + +If your change contains a merge commit, the above workflow may not work and you will need to remove the merge commit first. See the next section for details on how to rebase. + +Please refrain from creating several pull requests for the same change. Use the pull request that is already open (or was created earlier) to amend changes. This preserves the discussion and review that happened earlier for the respective change set. + +The length of time required for peer review is unpredictable and will vary from pull request to pull request. + +#### Refactoring + +Refactoring is a necessary part of any software project's evolution. The following guidelines cover refactoring pull requests for the Bittensor project. + +There are three categories of refactoring: code-only moves, code style fixes, and code refactoring. In general, refactoring pull requests should not mix these three kinds of activities in order to make refactoring pull requests easy to review and uncontroversial. In all cases, refactoring PRs must not change the behaviour of code within the pull request (bugs must be preserved as is). + +Project maintainers aim for a quick turnaround on refactoring pull requests, so where possible keep them short, uncomplex and easy to verify. + +Pull requests that refactor the code should not be made by new contributors. It requires a certain level of experience to know where the code belongs to and to understand the full ramification (including rebase effort of open pull requests). Trivial pull requests or pull requests that refactor the code with no clear benefits may be immediately closed by the maintainers to reduce unnecessary workload on reviewing. + +#### Peer Review + +Anyone may participate in peer review which is expressed by comments in the pull request. Typically reviewers will review the code for obvious errors, as well as test out the patch set and opine on the technical merits of the patch. Project maintainers take into account the peer review when determining if there is consensus to merge a pull request (remember that discussions may have taken place elsewhere, not just on GitHub). The following language is used within pull-request comments: + +- ACK means "I have tested the code and I agree it should be merged"; +- NACK means "I disagree this should be merged", and must be accompanied by sound technical justification. NACKs without accompanying reasoning may be disregarded; +- utACK means "I have not tested the code, but I have reviewed it and it looks OK, I agree it can be merged"; +- Concept ACK means "I agree in the general principle of this pull request"; +- Nit refers to trivial, often non-blocking issues. + +Reviewers should include the commit(s) they have reviewed in their comments. This can be done by copying the commit SHA1 hash. + +A pull request that changes consensus-critical code is considerably more involved than a pull request that adds a feature to the wallet, for example. Such patches must be reviewed and thoroughly tested by several reviewers who are knowledgeable about the changed subsystems. Where new features are proposed, it is helpful for reviewers to try out the patch set on a test network and indicate that they have done so in their review. Project maintainers will take this into consideration when merging changes. + +For a more detailed description of the review process, see the [Code Review Guidelines](CODE_REVIEW_DOCS.md). + +### Reporting Bugs + +This section guides you through submitting a bug report for Bittensor. Following these guidelines helps maintainers and the community understand your report :pencil:, reproduce the behavior :computer: :computer:, and find related reports :mag_right:. + +When you are creating a bug report, please [include as many details as possible](#how-do-i-submit-a-good-bug-report). + +> **Note:** If you find a **Closed** issue that seems like it is the same thing that you're experiencing, open a new issue and include a link to the original issue in the body of your new one. + +#### Before Submitting A Bug Report + +* **Check the [debugging guide](./DEBUGGING.md).** You might be able to find the cause of the problem and fix things yourself. Most importantly, check if you can reproduce the problem in the latest version of Bittensor by updating to the latest Master branch changes. +* **Check the [Discord Server](https://discord.gg/7wvFuPJZgq)** and ask in [#finney-issues](https://discord.com/channels/799672011265015819/1064247007688007800) or [#subnet-1-issues](https://discord.com/channels/799672011265015819/1096187495667998790). +* **Determine which repository the problem should be reported in**: if it has to do with your ML model, then it's likely [Bittensor](https://github.com/opentensor/bittensor). If you are having problems with your emissions or Blockchain, then it is in [subtensor](https://github.com/opentensor/subtensor) + +#### How Do I Submit A (Good) Bug Report? + +Bugs are tracked as [GitHub issues](https://guides.github.com/features/issues/). You can find Bittensor's issues [here](https://github.com/opentensor/bittensor/issues). After you've determined which repository ([Bittensor](https://github.com/opentensor/bittensor) or [subtensor](https://github.com/opentensor/subtensor)) your bug is related to, create an issue on that repository. + +Explain the problem and include additional details to help maintainers reproduce the problem: + +* **Use a clear and descriptive title** for the issue to identify the problem. +* **Describe the exact steps which reproduce the problem** in as many details as possible. For example, start by explaining how you started Bittensor, e.g. which command exactly you used in the terminal, or how you started Bittensor otherwise. When listing steps, **don't just say what you did, but explain how you did it**. For example, if you ran Bittensor with a set of custom configs, explain if you used a config file or command line arguments. +* **Provide specific examples to demonstrate the steps**. Include links to files or GitHub projects, or copy/pasteable snippets, which you use in those examples. If you're providing snippets in the issue, use [Markdown code blocks](https://help.github.com/articles/markdown-basics/#multiple-lines). +* **Describe the behavior you observed after following the steps** and point out what exactly is the problem with that behavior. +* **Explain which behavior you expected to see instead and why.** +* **Include screenshots and animated GIFs** which show you following the described steps and clearly demonstrate the problem. You can use [this tool](https://www.cockos.com/licecap/) to record GIFs on macOS and Windows, and [this tool](https://github.com/colinkeenan/silentcast) or [this tool](https://github.com/GNOME/byzanz) on Linux. +* **If you're reporting that Bittensor crashed**, include a crash report with a stack trace from the operating system. On macOS, the crash report will be available in `Console.app` under "Diagnostic and usage information" > "User diagnostic reports". Include the crash report in the issue in a [code block](https://help.github.com/articles/markdown-basics/#multiple-lines), a [file attachment](https://docs.github.com/articles/file-attachments-on-issues-and-pull-requests/), or put it in a [gist](https://gist.github.com/) and provide link to that gist. +* **If the problem is related to performance or memory**, include a CPU profile capture with your report, if you're using a GPU then include a GPU profile capture as well. Look into the [PyTorch Profiler](https://pytorch.org/tutorials/recipes/recipes/profiler_recipe.html) to look at memory usage of your model. +* **If the problem wasn't triggered by a specific action**, describe what you were doing before the problem happened and share more information using the guidelines below. + +Provide more context by answering these questions: + +* **Did the problem start happening recently** (e.g. after updating to a new version of Bittensor) or was this always a problem? +* If the problem started happening recently, **can you reproduce the problem in an older version of Bittensor?** +* **Can you reliably reproduce the issue?** If not, provide details about how often the problem happens and under which conditions it normally happens. + +Include details about your configuration and environment: + +* **Which version of Bittensor are you using?** You can get the version by checking for `__version__` in [`bittensor/bittensor/__init.py`](https://github.com/opentensor/bittensor/blob/master/bittensor/__init__.py#L30). This is not sufficient. Also add the commit hash of the branch you are on. +* **What commit hash are you on?** You can get the exact commit hash by checking `git log` and pasting the full commit hash. +* **What's the name and version of the OS you're using**? +* **Are you running Bittensor in a virtual machine?** If so, which VM software are you using and which operating systems and versions are used for the host and the guest? +* **Are you running Bittensor in a dockerized container?** If so, have you made sure that your docker container contains your latest changes and is up to date with Master branch? +* **Are you using [local configuration files](https://opentensor.github.io/getting-started/configuration.html)** `config.yaml` to customize your Bittensor experiment? If so, provide the contents of that config file, preferably in a [code block](https://help.github.com/articles/markdown-basics/#multiple-lines) or with a link to a [gist](https://gist.github.com/). + +### Suggesting Enhancements and Features + +This section guides you through submitting an enhancement suggestion for Bittensor, including completely new features and minor improvements to existing functionality. Following these guidelines helps maintainers and the community understand your suggestion :pencil: and find related suggestions :mag_right:. + +When you are creating an enhancement suggestion, please [include as many details as possible](#how-do-i-submit-a-good-enhancement-suggestion). + +#### Before Submitting An Enhancement Suggestion + +* **Check the [debugging guide](./DEBUGGING.md).** for tips — you might discover that the enhancement is already available. Most importantly, check if you're using the latest version of Bittensor by pulling the latest changes from the Master branch and if you can get the desired behavior by changing [Bittensor's config settings](https://docs.learnbittensor.org/python-api/html/autoapi/bittensor/core/config/). +* **Determine which repository the problem should be reported in: if it has to do with your ML model, then it's likely [Bittensor](https://github.com/opentensor/bittensor). If you are having problems with your emissions or Blockchain, then it is in [subtensor](https://github.com/opentensor/subtensor) + +#### How To Submit A (Good) Feature Suggestion + +Enhancement suggestions are tracked as [GitHub issues](https://guides.github.com/features/issues/). After you've determined which repository ([Bittensor](https://github.com/opentensor/bittensor) or [subtensor](https://github.com/opentensor/subtensor)) your enhancement suggestion is related to, create an issue on that repository and provide the following information: + +* **Use a clear and descriptive title** for the issue to identify the problem. +* **Provide a step-by-step description of the suggested enhancement** in as many details as possible. +* **Provide specific examples to demonstrate the steps**. Include copy/pasteable snippets which you use in those examples, as [Markdown code blocks](https://help.github.com/articles/markdown-basics/#multiple-lines). +* **Describe the current behavior** and **explain which behavior you expected to see instead** and why. +* **Include screenshots and animated GIFs** which help you demonstrate the steps or point out the part of Bittensor which the suggestion is related to. You can use [this tool](https://www.cockos.com/licecap/) to record GIFs on macOS and Windows, and [this tool](https://github.com/colinkeenan/silentcast) or [this tool](https://github.com/GNOME/byzanz) on Linux. +* **Explain why this enhancement would be useful** to most Bittensor users. +* **List some other text editors or applications where this enhancement exists.** +* **Specify which version of Bittensor are you using?** You can get the exact version by checking for `__version__` in [`bittensor/bittensor/__init.py`](https://github.com/opentensor/bittensor/blob/master/bittensor/__init__.py#L30). +* **Specify the name and version of the OS you're using.** + +Thank you for considering contributing to Bittensor! Any help is greatly appreciated along this journey to incentivize open and permissionless intelligence. diff --git a/contrib/DEBUGGING.md b/contrib/DEBUGGING.md new file mode 100644 index 0000000000..22013abcb8 --- /dev/null +++ b/contrib/DEBUGGING.md @@ -0,0 +1,161 @@ +## Installation + +First, make sure you have Bittensor installed correctly. There are three ways to install Bittensor: + +1. Through the installer: + +```bash +/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/opentensor/bittensor/master/scripts/install.sh)" +``` + +2. With pip: + +```bash +pip install bittensor +``` + +3. From source: + +```bash +git clone https://github.com/opentensor/bittensor.git +python3 -m pip install -e bittensor/ +``` + +You can test your installation by running: + +```bash +python3 -c "import bittensor; print(bittensor.__version__)" +``` +## Logging +Make good use of the `bittensor.logging` module. It can be your friend and will help you find things that are otherwise difficult to get visibility on. + +You can enable debug or trace modes by running: +``` +import bittensor +bittensor.trace() # lowest level of granularity, best for figuring out what went wrong. +bittensor.debug() # for most everything else that you don't want to see normally at runtime +``` +at the top of your script or source file to enable more verbose output logs. + +You can also write your own in the code simply: +```python +# Bittensor's wallet maintenance class. +wallet = bittensor.wallet() + +bittensor.logging.debug( f"wallet keypair: {wallet.hotkey}" ) + +... + +# Bittensor's chain state object. +metagraph = bittensor.metagraph(netuid=1) + +bittensor.logging.trace( f"metagraph created! netuid {metagraph.netuid}" ) +``` + + +## Querying the Network + +Ensure you can query the Bittensor network using the Python API. If something is broken with your installation or the chain, this won't work out of the box. Here's an example of how to do this: + +```python +import bittensor +bittensor.trace() + +# Attempt to query through the foundation endpoint. +print(bittensor.prompt("Heraclitus was a ")) +``` + +## Debugging Miners + + +First, try registering and running on a testnet: +```bash +btcli register --netuid --subtensor.chain_endpoint wss://test.finney.opentensor.ai:443 +``` + +If that works, then try to register a miner on mainnet: + +```bash +btcli register --netuid +``` + +See if you can observe your slot specified by UID: + +```bash +btcli overview --netuid +``` + +Here's an example of how to run a pre-configured miner: + +```bash +python3 bittensor/neurons/text_prompting/miners/GPT4ALL/neuron.py --netuid +``` + +## Debugging with the Bittensor Package + +The Bittensor package contains data structures for interacting with the Bittensor ecosystem, writing miners, validators, and querying the network. + +Try to use the Bittensor package to create a wallet, connect to the axon running on slot 10, and send a prompt to this endpoint and see where things are breaking along this typical codepath: + +```python +import bittensor + +# Bittensor's wallet maintenance class. +wallet = bittensor.wallet() + +# Bittensor's chain interface. +subtensor = bittensor.subtensor() + +# Bittensor's chain state object. +metagraph = bittensor.metagraph(netuid=1) + +# Instantiate a Bittensor endpoint. +axon = bittensor.axon(wallet=wallet, metagraph=metagraph) + +# Start servicing messages on the wire. +axon.start() + +# Register this axon on a subnetwork +subtensor.serve_axon(netuid=1, axon=axon) + +# Connect to the axon running on slot 10, use the wallet to sign messages. +dendrite = bittensor.text_prompting(keypair=wallet.hotkey, axon=metagraph.axons[10]) + +# Send a prompt to this endpoint +dendrite.forward(roles=['user'], messages=['Who is Rick James?']) +``` + +> NOTE: It may be helpful to throw in breakpoints such as with `pdb`. +```python +# some code ... +import pdb; pdb.set_trace() # breakpoint! +# more code ... + +``` +This will stop execution at the breakpoint you set and can operate on the stack directly in the terminal. + +## Searching for strings +Use `ag`. It's fast, convenient, and widely available on unix systems. Ag will highlight all occurrences of a given pattern. + +```bash +apt-get install silversearcher-ag +``` + +Usage: +```bash +$ ag "query_subtensor" + +>>> bittensor/_subtensor/subtensor_mock.py +>>> 165: e.g. We mock `Subtensor.query_subtensor` instead of all query methods. +>>> 536: def query_subtensor( +>>> 1149: curr_total_hotkey_stake = self.query_subtensor( +>>> 1154: curr_total_coldkey_stake = self.query_subtensor( +>>> 1345: return self.query_subtensor(name=name, block=block, params=[netuid]).value +>>> +>>> bittensor/_subtensor/subtensor_impl.py +>>> 902: def query_subtensor( +>>> 1017: return self.query_subtensor("Rho", block, [netuid]).value +... +``` + +Remember, debugging involves a lot of trial and error. Don't be discouraged if things don't work right away. Keep trying different things, and don't hesitate to ask for help if you need it. diff --git a/contrib/DEVELOPMENT_WORKFLOW.md b/contrib/DEVELOPMENT_WORKFLOW.md new file mode 100644 index 0000000000..91e781ffcc --- /dev/null +++ b/contrib/DEVELOPMENT_WORKFLOW.md @@ -0,0 +1,159 @@ +# Bittensor Development Workflow + +## Table of contents + +- [Bittensor Development Workflow](#bittensor-development-workflow) + - [Main Branches](#main-branches) + - [Development Model](#development-model) + - [Feature Branches](#feature-branches) + - [Release Branches](#release-branches) + - [Hotfix Branches](#hotfix-branches) + - [Git Operations](#git-operations) + - [Creating a Feature Branch](#creating-a-feature-branch) + - [Merging Feature Branch into Staging](#merging-feature-branch-into-staging) + - [Creating a Release Branch](#creating-a-release-branch) + - [Finishing a Release Branch](#finishing-a-release-branch) + - [Creating a Hotfix Branch](#creating-a-hotfix-branch) + - [Finishing a Hotfix Branch](#finishing-a-hotfix-branch) + - [Continuous Integration (CI) and Continuous Deployment (CD)](#continuous-integration-ci-and-continuous-deployment-cd) + - [Versioning and Release Notes](#versioning-and-release-notes) + - [Pending Tasks](#pending-tasks) + +## Main Branches + +Bittensor's codebase consists of two main branches: **master** and **staging**. + +**master** +- This is Bittensor's live production branch, which should only be updated by the core development team. This branch is protected, so refrain from pushing or merging into it unless authorized. + +**staging** +- This branch is continuously updated and is where you propose and merge changes. It's essentially Bittensor's active development branch. + +## Development Model + +### Feature Branches + +- Branch off from: `staging` +- Merge back into: `staging` +- Naming convention: `feature//` + +Feature branches are used to develop new features for upcoming or future releases. They exist as long as the feature is in development, but will eventually be merged into `staging` or discarded. Always delete your feature branch after merging to avoid unnecessary clutter. + +### Release Branches + +- Branch off from: `staging` +- Merge back into: `staging` and then `master` +- Naming convention: `release///` + +Release branches support the preparation of a new production release, allowing for minor bug fixes and preparation of metadata (version number, configuration, etc). All new features should be merged into `staging` and wait for the next big release. + +### Hotfix Branches + +General workflow: + +- Branch off from: `master` or `staging` +- Merge back into: `staging` then `master` +- Naming convention: `hotfix///` + +Hotfix branches are meant for quick fixes in the production environment. When a critical bug in a production version must be resolved immediately, a hotfix branch is created. + +## Git Operations + +#### Create a feature branch + +1. Branch from the **staging** branch. + 1. Command: `git checkout -b feature/my-feature staging` + +> Rebase frequently with the updated staging branch so you do not face big conflicts before submitting your pull request. Remember, syncing your changes with other developers could also help you avoid big conflicts. + +#### Merge feature branch into staging + +In other words, integrate your changes into a branch that will be tested and prepared for release. + +1. Switch branch to staging: `git checkout staging` +2. Merging feature branch into staging: `git merge --no-ff feature/my-feature` +3. Pushing changes to staging: `git push origin staging` +4. Delete feature branch: `git branch -d feature/my-feature` (alternatively, this can be navigated on the GitHub web UI) + +This operation is done by Github when merging a PR. + +So, what you have to keep in mind is: +- Open the PR against the `staging` branch. +- After merging a PR you should delete your feature branch. This will be strictly enforced. + +#### Creating a release branch + +1. Create branch from staging: `git checkout -b release/3.4.0/descriptive-message/creator's_name staging` +2. Updating version with major or minor: `./scripts/update_version.sh major|minor` +3. Commit file changes with new version: `git commit -a -m "Updated version to 3.4.0"` + + +#### Finishing a Release Branch + +This involves releasing stable code and generating a new version for bittensor. + +1. Switch branch to master: `git checkout master` +2. Merge release branch into master: `git merge --no-ff release/3.4.0/optional-descriptive-message` +3. Tag changeset: `git tag -a v3.4.0 -m "Releasing v3.4.0: some comment about it"` +4. Push changes to master: `git push origin master` +5. Push tags to origin: `git push origin --tags` + +To keep the changes made in the __release__ branch, we need to merge those back into `staging`: + +- Switch branch to staging: `git checkout staging`. +- Merging release branch into staging: `git merge --no-ff release/3.4.0/optional-descriptive-message` + +This step may well lead to a merge conflict (probably even, since we have changed the version number). If so, fix it and commit. + + +#### Creating a hotfix branch +1. Create branch from master: `git checkout -b hotfix/3.3.4/descriptive-message/creator's-name master` +2. Update patch version: `./scripts/update_version.sh patch` +3. Commit file changes with new version: `git commit -a -m "Updated version to 3.3.4"` +4. Fix the bug and commit the fix: `git commit -m "Fixed critical production issue X"` + +#### Finishing a Hotfix Branch + +Finishing a hotfix branch involves merging the bugfix into both `master` and `staging`. + +1. Switch branch to master: `git checkout master` +2. Merge hotfix into master: `git merge --no-ff hotfix/3.3.4/optional-descriptive-message` +3. Tag new version: `git tag -a v3.3.4 -m "Releasing v3.3.4: descriptive comment about the hotfix"` +4. Push changes to master: `git push origin master` +5. Push tags to origin: `git push origin --tags` +6. Switch branch to staging: `git checkout staging` +7. Merge hotfix into staging: `git merge --no-ff hotfix/3.3.4/descriptive-message/creator's-name` +8. Push changes to origin/staging: `git push origin staging` +9. Delete hotfix branch: `git branch -d hotfix/3.3.4/optional-descriptive-message` + +The one exception to the rule here is that, **when a release branch currently exists, the hotfix changes need to be merged into that release branch, instead of** `staging`. Back-merging the bugfix into the __release__ branch will eventually result in the bugfix being merged into `develop` too, when the release branch is finished. (If work in develop immediately requires this bugfix and cannot wait for the release branch to be finished, you may safely merge the bugfix into develop now already as well.) + +Finally, we remove the temporary branch: + +- `git branch -d hotfix/3.3.4/optional-descriptive-message` +## Continuous Integration (CI) and Continuous Deployment (CD) + +Continuous Integration (CI) is a software development practice where members of a team integrate their work frequently. Each integration is verified by an automated build and test process to detect integration errors as quickly as possible. + +Continuous Deployment (CD) is a software engineering approach in which software functionalities are delivered frequently through automated deployments. + +- **CircleCI job**: Create jobs in CircleCI to automate the merging of staging into master and release version (needed to release code) and building and testing Bittensor (needed to merge PRs). + +## Versioning and Release Notes + +Semantic versioning helps keep track of the different versions of the software. When code is merged into master, generate a new version. + +Release notes provide documentation for each version released to the users, highlighting the new features, improvements, and bug fixes. When merged into master, generate GitHub release and release notes. + +## Pending Tasks + +- Determine if master and staging are different +- Determine what is in staging that is not merged yet + - Document not released developments + - When merged into staging, generate information about what's merged into staging but not released. + - When merged into master, generate GitHub release and release notes. +- CircleCI jobs + - Merge staging into master and release version (needed to release code) + - Build and Test Bittensor (needed to merge PRs) + +This document can be improved as the Bittensor project continues to develop and change. diff --git a/contrib/RELEASE_GUIDELINES.md b/contrib/RELEASE_GUIDELINES.md new file mode 100644 index 0000000000..f40003bd68 --- /dev/null +++ b/contrib/RELEASE_GUIDELINES.md @@ -0,0 +1,87 @@ +# Release Guidelines + +The release manager in charge can release a Bittensor version using two scripts: + - [../scripts/release/versioning.sh](../scripts/release/versioning.sh) + - [../scripts/release/release.sh](../scripts/release/release.sh) + +The release manager will need the right permissions for: + - github.com + - pypi.org + - hub.docker.com + +If you are new in this role, ask for the proper setup you need to run this process manually. + +## Process of release + +1. Create a branch called `release/VERSION`, having VERSION with the version to release. +1. Make sure twine is installed: `pip install twine` +1. Within the release branch: + 1. Update the version executing:`./scripts/release/versioning.sh --update UPDATE_TYPE` + 1. **UPDATE_TYPE** could be *major*, *minor* or *patch*. + 1. Add release notes to CHANGELOG executing: `./scripts/release/add_notes_changelog.sh -A -V NEW_VERSION -P PREVIOUS_TAG -T GH_ACCESS_TOKEN` + 1. **NEW_VERSION**: e.g.: 3.6.4 + 1. **PREVIOUS_TAG**: e.g.: v3.6.3 + 1. **GH_ACCESS_TOKEN**: A github [personal access token](https://docs.github.com/en/enterprise-server@3.4/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token) you need. + +1. Test the release branch and verify that it meets the requirements. +1. After merging the release branch; Run the release script + +## Versioning script usage + +Options: + - -U, --update: type of update. It could be major, minor, patch or rc (release candidate). + - -A, --apply: This specifies to apply the release. Without this the versioning will just show a dry run with no changes. + +## Release script usage + +Options: + - -A, --apply: This specifies to apply the release. Without this the release will just show a dry run with no changes. + - -T,--github-token: A github personal access token to interact with the Github API. + +### Github token + +Since you need to use a secret when releasing bittensor (github personal access token), I encourage you to use [pass](https://www.passwordstore.org/) or a similar tool that allows you to store the secret safely and not expose it in the history of the machine you use. + +So you can have: +``` +GITHUB_ACCESS_TOKEN=$(pass github/your_personal_token_with_permissions) +``` + +or +``` +GITHUB_ACCESS_TOKEN=$(whatever you need to get the token safely) +``` + +### Executions + +So, executing the script to release a minor version will be: + +``` +# For a dry run +./scripts/release/release.sh +``` + +``` +# Applying changes +./scripts/release/release.sh --apply --github-token $GITHUB_ACCESS_TOKEN` +``` + +## Checking release + +After the execution of the release script we would have generated: + - A new git tag in [github.com](https://github.com/opentensor/bittensor/tags) + - A new github release in [github.com](https://github.com/opentensor/bittensor/releases) + - A new pip package in [pypi.org](https://pypi.org/project/bittensor/#history) + - A new docker image in [hub.docker.com](https://hub.docker.com/r/opentensorfdn/bittensor/tags) + +## After release + +After a Bittensor release we have to +- Update [cubit](https://github.com/opentensor/cubit). + +### Updating cubit + +1. Updating the [Dockerfile](https://github.com/opentensor/cubit/blob/master/docker/Dockerfile) +1. Building its docker image (follow its README instructions) +1. Push it to hub.docker.com + 1. The generated name will be the same but with `-cubit` in its name diff --git a/contrib/STYLE.md b/contrib/STYLE.md new file mode 100644 index 0000000000..13558ac9e4 --- /dev/null +++ b/contrib/STYLE.md @@ -0,0 +1,350 @@ +# Style Guide + +A project’s long-term success rests (among other things) on its maintainability, and a maintainer has few tools more powerful than his or her project’s log. It’s worth taking the time to learn how to care for one properly. What may be a hassle at first soon becomes habit, and eventually a source of pride and productivity for all involved. + +Most programming languages have well-established conventions as to what constitutes idiomatic style, i.e. naming, formatting and so on. There are variations on these conventions, of course, but most developers agree that picking one and sticking to it is far better than the chaos that ensues when everybody does their own thing. + +# Table of Contents +1. [Code Style](#code-style) +2. [Naming Conventions](#naming-conventions) +3. [Git Commit Style](#git-commit-style) +4. [The Six Rules of a Great Commit](#the-six-rules-of-a-great-commit) + - [1. Atomic Commits](#1-atomic-commits) + - [2. Separate Subject from Body with a Blank Line](#2-separate-subject-from-body-with-a-blank-line) + - [3. Limit the Subject Line to 50 Characters](#3-limit-the-subject-line-to-50-characters) + - [4. Use the Imperative Mood in the Subject Line](#4-use-the-imperative-mood-in-the-subject-line) + - [5. Wrap the Body at 72 Characters](#5-wrap-the-body-at-72-characters) + - [6. Use the Body to Explain What and Why vs. How](#6-use-the-body-to-explain-what-and-why-vs-how) +5. [Tools Worth Mentioning](#tools-worth-mentioning) + - [Using `--fixup`](#using---fixup) + - [Interactive Rebase](#interactive-rebase) +6. [Pull Request and Squashing Commits Caveats](#pull-request-and-squashing-commits-caveats) + + +### Code style + +#### General Style +Python's official style guide is PEP 8, which provides conventions for writing code for the main Python distribution. Here are some key points: + +- `Indentation:` Use 4 spaces per indentation level. + +- `Line Length:` Limit all lines to a maximum of 79 characters. + +- `Blank Lines:` Surround top-level function and class definitions with two blank lines. Method definitions inside a class are surrounded by a single blank line. + +- `Imports:` Imports should usually be on separate lines and should be grouped in the following order: + + - Standard library imports. + - Related third party imports. + - Local application/library specific imports. +- `Whitespace:` Avoid extraneous whitespace in the following situations: + + - Immediately inside parentheses, brackets or braces. + - Immediately before a comma, semicolon, or colon. + - Immediately before the open parenthesis that starts the argument list of a function call. +- `Comments:` Comments should be complete sentences and should be used to clarify code and are not a substitute for poorly written code. + +#### For Python + +- `List Comprehensions:` Use list comprehensions for concise and readable creation of lists. + +- `Generators:` Use generators when dealing with large amounts of data to save memory. + +- `Context Managers:` Use context managers (with statement) for resource management. + +- `String Formatting:` Use f-strings for formatting strings in Python 3.6 and above. + +- `Error Handling:` Use exceptions for error handling whenever possible. + +#### More details + +Use [`ruff` to format](https://docs.astral.sh/ruff/formatter/#the-ruff-formatter) your python code before committing for consistency across such a large pool of contributors. +Black code [style](https://black.readthedocs.io/en/stable/the_black_code_style/current_style.html#code-style) ensures consistent and opinionated code formatting. +Ruff automatically formats your Python code according to the Black style guide, enhancing code readability and maintainability. + +Key Features of ruff & Black code style: + + Consistency: ruff enforces a single, consistent coding style across your project, eliminating style debates and allowing developers to focus on code logic. + + Readability: By applying a standard formatting style, Black improves code readability, making it easier to understand and collaborate on projects. + + Automation: ruff automates the code formatting process, saving time and effort. It eliminates the need for manual formatting and reduces the likelihood of inconsistencies. + +### Naming Conventions + +- `Classes:` Class names should normally use the CapWords Convention. +- `Functions and Variables:` Function names should be lowercase, with words separated by underscores as necessary to improve readability. Variable names follow the same convention as function names. + +- `Constants:` Constants are usually defined on a module level and written in all capital letters with underscores separating words. + +- `Non-public Methods and Instance Variables:` Use a single leading underscore (_). This is a weak "internal use" indicator. + +- `Strongly "private" methods and variables:` Use a double leading underscore (__). This triggers name mangling in Python. + + +### Git commit style + +Here’s a model Git commit message when contributing: +``` +Summarize changes in around 50 characters or less + +More detailed explanatory text, if necessary. Wrap it to about 72 +characters or so. In some contexts, the first line is treated as the +subject of the commit and the rest of the text as the body. The +blank line separating the summary from the body is critical (unless +you omit the body entirely); various tools like `log`, `shortlog` +and `rebase` can get confused if you run the two together. + +Explain the problem that this commit is solving. Focus on why you +are making this change as opposed to how (the code explains that). +Are there side effects or other unintuitive consequences of this +change? Here's the place to explain them. + +Further paragraphs come after blank lines. + + - Bullet points are okay, too + + - Typically a hyphen or asterisk is used for the bullet, preceded + by a single space, with blank lines in between, but conventions + vary here + +If you use an issue tracker, put references to them at the bottom, +like this: + +Resolves: #123 +See also: #456, #789 +``` + + +## The six rules of a great commit. + +#### 1. Atomic Commits +An “atomic” change revolves around one task or one fix. + +Atomic Approach + - Commit each fix or task as a separate change + - Only commit when a block of work is complete + - Commit each layout change separately + - Joint commit for layout file, code behind file, and additional resources + +Benefits + +- Easy to roll back without affecting other changes +- Easy to make other changes on the fly +- Easy to merge features to other branches + +#### Avoid trivial commit messages + +Commit messages like "fix", "fix2", or "fix3" don't provide any context or clear understanding of what changes the commit introduces. Here are some examples of good vs. bad commit messages: + +**Bad Commit Message:** + + $ git commit -m "fix" + +**Good Commit Message:** + + $ git commit -m "Fix typo in README file" + +> **Caveat**: When working with new features, an atomic commit will often consist of multiple files, since a layout file, code behind file, and additional resources may have been added/modified. You don’t want to commit all of these separately, because if you had to roll back the application to a state before the feature was added, it would involve multiple commit entries, and that can get confusing + +#### 2. Separate subject from body with a blank line + +Not every commit requires both a subject and a body. Sometimes a single line is fine, especially when the change is so simple that no further context is necessary. + +For example: + + Fix typo in introduction to user guide + +Nothing more needs to be said; if the reader wonders what the typo was, she can simply take a look at the change itself, i.e. use git show or git diff or git log -p. + +If you’re committing something like this at the command line, it’s easy to use the -m option to git commit: + + $ git commit -m"Fix typo in introduction to user guide" + +However, when a commit merits a bit of explanation and context, you need to write a body. For example: + + Derezz the master control program + + MCP turned out to be evil and had become intent on world domination. + This commit throws Tron's disc into MCP (causing its deresolution) + and turns it back into a chess game. + +Commit messages with bodies are not so easy to write with the -m option. You’re better off writing the message in a proper text editor. [See Pro Git](https://git-scm.com/book/en/v2/Customizing-Git-Git-Configuration). + +In any case, the separation of subject from body pays off when browsing the log. Here’s the full log entry: + + $ git log + commit 42e769bdf4894310333942ffc5a15151222a87be + Author: Kevin Flynn + Date: Fri Jan 01 00:00:00 1982 -0200 + + Derezz the master control program + + MCP turned out to be evil and had become intent on world domination. + This commit throws Tron's disc into MCP (causing its deresolution) + and turns it back into a chess game. + + +#### 3. Limit the subject line to 50 characters +50 characters is not a hard limit, just a rule of thumb. Keeping subject lines at this length ensures that they are readable, and forces the author to think for a moment about the most concise way to explain what’s going on. + +GitHub’s UI is fully aware of these conventions. It will warn you if you go past the 50 character limit. Git will truncate any subject line longer than 72 characters with an ellipsis, thus keeping it to 50 is best practice. + +#### 4. Use the imperative mood in the subject line +Imperative mood just means “spoken or written as if giving a command or instruction”. A few examples: + + Clean your room + Close the door + Take out the trash + +Each of the seven rules you’re reading about right now is written in the imperative (“Wrap the body at 72 characters”, etc.). + +The imperative can sound a little rude; that’s why we don’t often use it. But it’s perfect for Git commit subject lines. One reason for this is that Git itself uses the imperative whenever it creates a commit on your behalf. + +For example, the default message created when using git merge reads: + + Merge branch 'myfeature' + +And when using git revert: + + Revert "Add the thing with the stuff" + + This reverts commit cc87791524aedd593cff5a74532befe7ab69ce9d. + +Or when clicking the “Merge” button on a GitHub pull request: + + Merge pull request #123 from someuser/somebranch + +So when you write your commit messages in the imperative, you’re following Git’s own built-in conventions. For example: + + Refactor subsystem X for readability + Update getting started documentation + Remove deprecated methods + Release version 1.0.0 + +Writing this way can be a little awkward at first. We’re more used to speaking in the indicative mood, which is all about reporting facts. That’s why commit messages often end up reading like this: + + Fixed bug with Y + Changing behavior of X + +And sometimes commit messages get written as a description of their contents: + + More fixes for broken stuff + Sweet new API methods + +To remove any confusion, here’s a simple rule to get it right every time. + +**A properly formed Git commit subject line should always be able to complete the following sentence:** + + If applied, this commit will + +For example: + + If applied, this commit will refactor subsystem X for readability + If applied, this commit will update getting started documentation + If applied, this commit will remove deprecated methods + If applied, this commit will release version 1.0.0 + If applied, this commit will merge pull request #123 from user/branch + +#### 5. Wrap the body at 72 characters +Git never wraps text automatically. When you write the body of a commit message, you must mind its right margin, and wrap text manually. + +The recommendation is to do this at 72 characters, so that Git has plenty of room to indent text while still keeping everything under 80 characters overall. + +A good text editor can help here. It’s easy to configure Vim, for example, to wrap text at 72 characters when you’re writing a Git commit. + +#### 6. Use the body to explain what and why vs. how +This [commit](https://github.com/bitcoin/bitcoin/commit/eb0b56b19017ab5c16c745e6da39c53126924ed6) from Bitcoin Core is a great example of explaining what changed and why: + +``` +commit eb0b56b19017ab5c16c745e6da39c53126924ed6 +Author: Pieter Wuille +Date: Fri Aug 1 22:57:55 2014 +0200 + + Simplify serialize.h's exception handling + + Remove the 'state' and 'exceptmask' from serialize.h's stream + implementations, as well as related methods. + + As exceptmask always included 'failbit', and setstate was always + called with bits = failbit, all it did was immediately raise an + exception. Get rid of those variables, and replace the setstate + with direct exception throwing (which also removes some dead + code). + + As a result, good() is never reached after a failure (there are + only 2 calls, one of which is in tests), and can just be replaced + by !eof(). + + fail(), clear(n) and exceptions() are just never called. Delete + them. +``` + +Take a look at the [full diff](https://github.com/bitcoin/bitcoin/commit/eb0b56b19017ab5c16c745e6da39c53126924ed6) and just think how much time the author is saving fellow and future committers by taking the time to provide this context here and now. If he didn’t, it would probably be lost forever. + +In most cases, you can leave out details about how a change has been made. Code is generally self-explanatory in this regard (and if the code is so complex that it needs to be explained in prose, that’s what source comments are for). Just focus on making clear the reasons why you made the change in the first place—the way things worked before the change (and what was wrong with that), the way they work now, and why you decided to solve it the way you did. + +The future maintainer that thanks you may be yourself! + + + +#### Tools worth mentioning + +##### Using `--fixup` + +If you've made a commit and then realize you've missed something or made a minor mistake, you can use the `--fixup` option. + +For example, suppose you've made a commit with a hash `9fceb02`. Later, you realize you've left a debug statement in your code. Instead of making a new commit titled "remove debug statement" or "fix", you can do the following: + + $ git commit --fixup 9fceb02 + +This will create a new commit to fix the issue, with a message like "fixup! The original commit message". + +##### Interactive Rebase + +Interactive rebase, or `rebase -i`, can be used to squash these fixup commits into the original commits they're fixing, which cleans up your commit history. You can use the `autosquash` option to automatically squash any commits marked as "fixup" into their target commits. + +For example: + + $ git rebase -i --autosquash HEAD~5 + +This command starts an interactive rebase for the last 5 commits (`HEAD~5`). Any commits marked as "fixup" will be automatically moved to squash with their target commits. + +The benefit of using `--fixup` and interactive rebase is that it keeps your commit history clean and readable. It groups fixes with the commits they are related to, rather than having a separate "fix" commit that might not make sense to other developers (or even to you) in the future. + + +--- + +#### Pull Request and Squashing Commits Caveats + +While atomic commits are great for development and for understanding the changes within the branch, the commit history can get messy when merging to the main branch. To keep a cleaner and more understandable commit history in our main branch, we encourage squashing all the commits of a PR into one when merging. + +This single commit should provide an overview of the changes that the PR introduced. It should follow the guidelines for atomic commits (an atomic commit is complete, self-contained, and understandable) but on the scale of the entire feature, task, or fix that the PR addresses. This approach combines the benefits of atomic commits during development with a clean commit history in our main branch. + +Here is how you can squash commits: + +```bash +git rebase -i HEAD~n +``` + +where `n` is the number of commits to squash. After running the command, replace `pick` with `squash` for the commits you want to squash into the previous commit. This will combine the commits and allow you to write a new commit message. + +In this context, an atomic commit message could look like: + +``` +Add feature X + +This commit introduces feature X which does A, B, and C. It adds +new files for layout, updates the code behind the file, and introduces +new resources. This change is important because it allows users to +perform task Y more efficiently. + +It includes: +- Creation of new layout file +- Updates in the code-behind file +- Addition of new resources + +Resolves: #123 +``` + +In your PRs, remember to detail what the PR is introducing or fixing. This will be helpful for reviewers to understand the context and the reason behind the changes. diff --git a/contrib/TESTING.md b/contrib/TESTING.md new file mode 100644 index 0000000000..59dc1d81a3 --- /dev/null +++ b/contrib/TESTING.md @@ -0,0 +1,94 @@ +# Testing Guide for Bittensor + +Testing is an essential part of software development that ensures the correctness and performance of your code. Bittensor uses a combination of unit tests and integration tests to verify the functionality of its components. This guide will walk you through how to run and write tests for Bittensor. + +## Running Tests + +Bittensor uses `pytest` for running its tests. To run all tests, navigate to the root directory of the Bittensor repository and run: + +```bash +pytest +``` + +This will automatically discover all test files (those that start with `test_`) and run them. + +If you want to run a specific test file, you can specify it directly. For example, to run the tests in `test_wallet.py`, you would use: + +```bash +pytest tests/test_wallet.py +``` + +Similarly, you can run a specific test within a file by appending `::` and the test name. For example: + +```bash +pytest tests/test_wallet.py::test_create_new_coldkey +``` + +## Writing Tests + +When writing tests for Bittensor, you should aim to cover both the "happy path" (where everything works as expected) and any potential error conditions. Here's a basic structure for a test file: + +```python +import pytest +import bittensor + +def test_some_functionality(): + # Setup any necessary objects or state. + wallet = bittensor.wallet() + + # Call the function you're testing. + result = wallet.create_new_coldkey() + + # Assert that the function behaved as expected. + assert result is not None +``` + +In this example, we're testing the `create_new_coldkey` function of the `wallet` object. We assert that the result is not `None`, which is the expected behavior. + +## Mocking + +In some cases, you may need to mock certain functions or objects to isolate the functionality you're testing. Bittensor uses the `unittest.mock` library for this. Here's a simple example from the axon unittest: + +```python +def test_axon_start(self): + mock_wallet = MagicMock( + spec=bittensor.Wallet, + coldkey=MagicMock(), + coldkeypub=MagicMock( + # mock ss58 address + ss58_address="5DD26kC2kxajmwfbbZmVmxhrY9VeeyR1Gpzy9i8wxLUg6zxm" + ), + hotkey=MagicMock( + ss58_address="5CtstubuSoVLJGCXkiWRNKrrGg2DVBZ9qMs2qYTLsZR4q1Wg" + ), + ) + axon = bittensor.axon(wallet=mock_wallet, metagraph=None) + axon.start() + assert axon.server._state.stage == grpc._server._ServerStage.STARTED +``` + +In this example, we're mocking the `coldkey`, `coldkeypub` and `hotkey` for a wallet. This allows us to test how the axon code behaves when `bittensor.Wallet()` would normally be called, without actually calling the constructor. +## Test Coverage + +It's important to ensure that your tests cover as much of your code as possible. You can use the `pytest-cov` plugin to measure your test coverage. To use it, first install it with pip: + +```bash +pip install pytest-cov +``` + +Then, you can run your tests with coverage like this: + +```bash +pytest --cov=bittensor +``` + +This will output a coverage report showing the percentage of your code that's covered by tests. + +Remember, while high test coverage is a good goal, it's also important to write meaningful tests. A test isn't very useful if it doesn't accurately represent the conditions under which your code will run. + +## Continuous Integration + +Bittensor uses CircleCI for continuous integration. This means that every time you push changes to the repository, all tests are automatically run. If any tests fail, you'll be notified so you can fix the issue before merging your changes. + + +Remember, tests are an important part of maintaining the health of a codebase. They help catch issues early and make it easier to add new features or refactor existing code. Happy testing! \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000000..7e6933ed25 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,10 @@ +version: "3.2" + +services: + dev: + container_name: node-bittensor + image: "bittensor/bittensor:latest" + ports: + - "8091:8091" + volumes: + - ~/.bittensor:/root/.bittensor \ No newline at end of file diff --git a/example.env b/example.env new file mode 100644 index 0000000000..35d405fb58 --- /dev/null +++ b/example.env @@ -0,0 +1,5 @@ +# To use legacy Torch-based of bittensor, you must set USE_TORCH=1 +USE_TORCH=0 +# If set to 0 (or anything else than 1), it will use current, numpy-based, bittensor interface. +# This is generally what you want unless you want legacy interoperability. +# Please note that the legacy interface is deprecated, and is not tested nearly as much. diff --git a/migration.md b/migration.md new file mode 100644 index 0000000000..3e4b0a7a1a --- /dev/null +++ b/migration.md @@ -0,0 +1,226 @@ +# Plan + +## Extrinsics and related +1. ✅ Standardize parameter order across all extrinsics and related calls. Pass extrinsic-specific arguments first (e.g., wallet, hotkey, netuid, amount), followed by optional general flags (e.g., wait_for_inclusion, wait_for_finalization) +
+ Example + + ```py + def swap_stake_extrinsic( + subtensor: "Subtensor", + wallet: "Wallet", + hotkey_ss58: str, + origin_netuid: int, + destination_netuid: int, + amount: Optional[Balance] = None, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, + safe_staking: bool = False, + allow_partial_stake: bool = False, + rate_tolerance: float = 0.005, + period: Optional[int] = None, + raise_error: bool = True, + ) -> bool: + ``` + it will be + ```py + def swap_stake_extrinsic( + subtensor: "Subtensor", + wallet: "Wallet", + hotkey_ss58: str, + origin_netuid: int, + destination_netuid: int, + amount: Optional[Balance] = None, + rate_tolerance: float = 0.005, + allow_partial_stake: bool = False, + safe_swapping: bool = False, + period: Optional[int] = None, + raise_error: bool = True, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, + ) -> bool: + ``` +
+ +2. Unify extrinsic return values by introducing an ExtrinsicResponse class. Extrinsics currently return either a boolean or a tuple. + + Purpose: + - Ease of processing + - This class should contain success, message, and optionally data and logs. (to save all logs during the extrinsic) + +3. ✅ Set `wait_for_inclusion` and `wait_for_finalization` to `True` by default in extrinsics and their related calls. Then we will guarantee the correct/expected extrinsic call response is consistent with the chain response. If the user changes those values, then it is the user's responsibility. +4. ✅ Make the internal logic of extrinsics the same. There are extrinsics that are slightly different in implementation. + +5. Since SDK is not a responsible tool, try to remove all calculations inside extrinsics that do not affect the result, but are only used in logging. Actually, this should be applied not to extrinsics only but for all codebase. + +6. ✅ Remove `unstake_all` parameter from `unstake_extrinsic` since we have `unstake_all_extrinsic`which is calles another subtensor function. + +7. ✅ `unstake` and `unstake_multiple` extrinsics should have `safe_unstaking` parameters instead of `safe_staking`. + +8. ✅ Remove `_do*` extrinsic calls and combine them with extrinsic logic. + +9. `subtensor.get_transfer_fee` calls extrinsic inside the subtensor module. Actually the method could be updated by using `bittensor.core.extrinsics.utils.get_extrinsic_fee`. + +## Subtensor +1. In the synchronous Subtensor class, the `get_owned_hotkeys` method includes a `reuse_block` parameter that is inconsistent with other methods. Either remove this parameter from `get_owned_hotkeys`, or add it to all other methods that directly call self.substrate.* to maintain a consistent interface. +2. In all methods where we `get_stake_operations_fee` is called, remove unused arguments. Consider combining all methods using `get_stake_operations_fee` into one common one. +3. Delete deprecated `get_current_weight_commit_info` and `get_current_weight_commit_info_v2`. Rename `get_timelocked_weight_commits` to get_current_weight_commit_info. +4. Remove references like `get_stake_info_for_coldkey = get_stake_for_coldkey`. +5. Reconsider some methods naming across the entire subtensor module. +6. Add `hotkey_ss58` parameter to `get_liquidity_list` method. One wallet can have many HKs. Currently, the mentioned method uses default HK only. + +## Metagraph +1. Remove verbose archival node warnings for blocks older than 300. Some users complained about many messages for them. +2. Reconsider entire metagraph module logic. + +## Balance +1. In `bittensor.utils.balance._check_currencies` raise the error instead of `warnings.warn`. +2. In `bittensor.utils.balance.check_and_convert_to_balance` raise the error instead of `warnings.warn`. +This may seem like a harsh decision at first, but ultimately we will push the community to use Balance and there will be fewer errors in their calculations. Confusion with TAO and Alpha in calculations and display/printing/logging will be eliminated. + +## Common things +1. Reduce the amount of logging.info or transfer part of logging.info to logging.debug + +2. To be consistent across all SDK regarding local environment variables name: +remove `BT_CHAIN_ENDPOINT` (settings.py :line 124) and use `BT_SUBTENSOR_CHAIN_ENDPOINT` instead of that. +rename this variable in documentation. + +3. Move `bittensor.utils.get_transfer_fn_params` to `bittensor.core.extrinsics.utils`. + +4. Common refactoring (improve type annotations, etc) + +5. Rename `non-/fast-blocks` to `non-/fast-runtime` in related places to be consistent with subtensor repo. Related with testing, subtensor scripts, documentation. + +6. To be consistent throughout the SDK: +`hotkey`, `coldkey`, `hotkeypub`, and `coldkeypub` are keypairs +`hotkey_ss58`, `coldkey_ss58`, `hotkeypub_ss58`, and `coldkeypub_ss58` are SS58 addresses of keypair. + +7. Replace `Arguments` with `Parameters`. Matches Python rules. Improve docstrings for writing MСP server. + +8. Remove all type annotations for parameters in docstrings. + +9. Remove all logic related to CRv3 as it will be removed from the chain next week. + +10. Revise `bittensor/utils/easy_imports.py` module to remove deprecated backwards compatibility objects. Use this module as a functionality for exporting existing objects to the package root to keep __init__.py minimal and simple. + +11. Remove `bittensor.utils.version.version_checking` + +12. Find and process all `TODOs` across the entire code base. If in doubt, discuss each one with the team separately. SDK has 29 TODOs. +13. ✅ The SDK is dropping support for `Python 3.9` starting with this release. +14. Remove `Default is` and `Default to` in docstrings bc parameters enough. +15. camfairchild: TODO, but we should have a grab_metadata if we don't already. Maybe don't decode, but can have a call that removes the Raw prefix, and another just doing grab_metadata_raw (no decoding) + +## New features +1. Add `bittensor.utils.hex_to_ss58` function. SDK still doesn't have it. (Probably inner import `from scalecodec import ss58_encode, ss58_decode`) +2. Implement Crowdloan logic. +3. “Implement Sub-subnets / Metagraph Changes?” (implementation unsure) Maciej Kula idea, requires mode details. + +## Testing +1. When running tests via Docker, ensure no lingering processes occupy required ports before launch. + +2. Improve failed test reporting from GH Actions to the Docker channel (e.g., clearer messages, formatting). + +3. Write a configurable test harness class for tests that will accept arguments and immediately: +create a subnet +activate a subnet (if the argument is passed as True) +register neurons (use wallets as arguments) +set the necessary hyperparameters (tempo, etc. if the argument are passed) +Will greatly simplify tests. + +4. Add an async test versions. This will help us greatly improve the asynchronous implementation of Subtensors and Extrinsics. + + +## Implementation + +To implement the above changes and prepare for the v10 release, the following steps must be taken: + +- [x] Create a new branch named SDKv10.~~ +All breaking changes and refactors should be targeted into this branch to isolate them from staging and maintain backward compatibility during development. +- [ ] Add a `migration.md` document at the root of the repository and use it as a check list. This file will serve as a changelog and technical reference. +It must include: + - [ ] All change categories (Extrinsics, Subtensor, Metagraph, etc.) + - [ ] Per-PR breakdown of what was added, removed, renamed, or refactored. + - [ ] Justifications and migration notes for users (if API behavior changed). + +- [ ] Based on the final `migration.md`, develop migration documentation for the community. +- [ ] Once complete, merge SDKv10 into staging and release version 10. + + +# Migration guide + +- [x] `._do_commit_reveal_v3` logic is included in the main code `.commit_reveal_v3_extrinsic` +- [x] `.commit_reveal_v3_extrinsic` renamed to `.commit_reveal_extrinsic` +- [x] `revecommit_reveal_version` parameter with default value `4` added to `revecommit_reveal_version` +- [x] `._do_commit_weights` logic is included in the main code `.commit_weights_extrinsic` +- [x] `._do_reveal_weights` logic is included in the main code `.reveal_weights_extrinsic` +- [x] `._do_set_weights` logic is included in the main code `.set_weights_extrinsic` +- [x] `set_weights_extrinsic` moved to `bittensor/core/extrinsics/commit_weights.py` +- [x] `bittensor/core/extrinsics/commit_weights.py` module renamed to `bittensor/core/extrinsics/weights.py` (consistent naming with async module) +- [x] `_do_burned_register` logic is included in the main code `.burned_register_extrinsic` +- [x] `_do_pow_register` logic is included in the main code `.register_extrinsic` +- [x] `._do_set_root_weights` logic is included in the main code `.set_root_weights_extrinsic` +- [x] `._do_transfer` logic is included in the main code `.transfer_extrinsic` +- [x] `dest` parameter has been renamed to `destination` in `transfer_extrinsic` function and `subtensor.transfer` method. +- [x] obsolete extrinsic `set_root_weights_extrinsic` removed. Also related subtensor calls `subtensor.set_root_weights_extrinsic` removed too. + +# Standardize parameter order is applied for (extrinsics and related calls): + +These parameters will now exist in all extrinsics and related calls (default values could be different depends by extrinsic): + +```py +period: Optional[int] = None, +raise_error: bool = False, +wait_for_inclusion: bool = False, +wait_for_finalization: bool = False, +``` +- [x] `.set_children_extrinsic` and `.root_set_pending_childkey_cooldown_extrinsic`. `subtensor.set_children` and `subtensor.root_set_pending_childkey_cooldown` methods. +- [x] `.commit_reveal_extrinsic` and `subtensor.set_weights` +- [x] `.add_liquidity_extrinsic` and `subtensor.add_liquidity` +- [x] `.modify_liquidity_extrinsic` and `subtensor.modify_liquidity` +- [x] `.remove_liquidity_extrinsic` and `subtensor.remove_liquidity` +- [x] `.toggle_user_liquidity_extrinsic` and `subtensor.toggle_user_liquidity` +- [x] `.transfer_stake_extrinsic` and `subtensor.transfer_stake` +- [x] `.swap_stake_extrinsic` and `subtensor.swap_stake` + - Changes in `swap_stake_extrinsic` and `subtensor.swap_stake`: + - parameter `safe_staking: bool` renamed to `safe_swapping: bool` +- [x] `.move_stake_extrinsic` and `subtensor.move_stake` + - Changes in `move_stake_extrinsic` and `subtensor.move_stake`: + - parameter `origin_hotkey` renamed to `origin_hotkey_ss58` + - parameter `destination_hotkey` renamed to `destination_hotkey_ss58` +- [x] `.burned_register_extrinsic` and `subtensor.burned_register` +- [x] `.register_subnet_extrinsic` and `subtensor.register_subnet` +- [x] `.register_extrinsic` and `subtensor.register` +- [x] `.set_subnet_identity_extrinsic` and `subtensor.set_subnet_identity` +- [x] `.root_register_extrinsic`, `subtensor.burned_register` and `subtensor.root_register` +- [x] `.serve_extrinsic` +- [x] `.serve_axon_extrinsic` and `subtensor.serve_axon` +- [x] alias `subtensor.set_commitment` removed +- [x] `subtensor.comit` renamed to `subtensor.set_commitment` +- [x] `.publish_metadata`, `subtensor.set_commitment` and `subtenor.set_reveal_commitment` +- [x] `.add_stake_extrinsic` and `subtensor.add_stake` + - Changes in `.add_stake_extrinsic` and `subtensor.add_stake`: + - parameter `old_balance` removed from async version + - parameter `netuid` required (no Optional anymore) + - parameter `hotkey_ss58` required (no Optional anymore) + - parameter `amount` required (no Optional anymore) +- [x] `.add_stake_multiple_extrinsic` and `subtensor.add_stake_multiple` + - Changes in `.add_stake_multiple_extrinsic` and `subtensor.add_stake_multiple`: + - parameter `old_balance` removed from async version + - parameter `amounts` required (no Optional anymore) +- [x] `.start_call_extrinsic` and `subtensor.start_call` +- [x] `.increase_take_extrinsic`, `.decrease_take_extrinsic` and `subtenor.set_reveal_commitment` +- [x] `.transfer_extrinsic` and `subtensor.transfer` +- [x] `.unstake_extrinsic` and `subtensor.unstake` + - Changes in `unstake_extrinsic` and `subtensor.unstake`: + - parameter `netuid: Optional[int]` is now required -> `netuid: int` + - parameter `hotkey_ss58: Optional[str]` is now required -> `hotkey_ss58: str` + - parameter `amount: Optional[Balance]` is now required -> `amount: Balance` + - parameter `safe_staking: bool` renamed to `safe_unstaking: bool` + - parameter `unstake_all: bool` removed (use `unstake_all_extrinsic` for unstake all stake) +- [x] `.unstake_all_extrinsic` and `subtensor.unstake_all` +- [x] `.unstake_multiple_extrinsic` and `subtensor.unstake_multiple` + - Changes in `.unstake_multiple_extrinsic` and `subtensor.unstake_multiple`: + - parameter `amounts` is now required (no Optional anymore) +- [x] `.commit_weights_extrinsic` and `subtensor.commit_weights` +- [x] `.reveal_weights_extrinsic` and `subtensor.reveal_weights` +- [x] `.set_weights_extrinsic` and `subtensor.set_weights` \ No newline at end of file diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 0000000000..d38bdc7172 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,18 @@ +[mypy] +ignore_missing_imports = True +ignore_errors = True + +[mypy-*.axon.*] +ignore_errors = False + +[mypy-*.dendrite.*] +ignore_errors = False + +[mypy-bittensor.metagraph.*] +ignore_errors = False + +[mypy-*.subtensor.*] +ignore_errors = False + +[mypy-*.synapse.*] +ignore_errors = False diff --git a/neurons/adam.py b/neurons/adam.py deleted file mode 100644 index e1fa99b7f7..0000000000 --- a/neurons/adam.py +++ /dev/null @@ -1,181 +0,0 @@ -#!/bin/python3 -"""Adam GPT2 Language Modelling miner - -This is the genesis miner of Bittensor, built to run indefinitely to mine tokens and learn as it runs GPT2. - -Example: - $ python neurons/gpt2-wiki.py - -""" -import argparse -import math -import os -import time -import torch -import torch.nn.functional as F -import traceback -import time -import bittensor - -from termcolor import colored -from munch import Munch -from datasets import load_dataset -from loguru import logger -from torch.utils.tensorboard import SummaryWriter - -from bittensor import Session -from bittensor.utils.logging import log_all -from bittensor.config import Config -from bittensor.synapses.gpt2 import GPT2LMSynapse, nextbatch -from pytorch_transformers import WarmupLinearSchedule - -def add_args(parser: argparse.ArgumentParser): - parser.add_argument('--neuron.datapath', default='data', type=str,help='Path to load and save data.') - parser.add_argument('--neuron.learning_rate', default=0.01, type=float, help='Training initial learning rate.') - parser.add_argument('--neuron.momentum', default=0.98, type=float, help='Training initial momentum for SGD.') - parser.add_argument('--neuron.batch_size_train', default=20, type=int, help='Training batch size.') - parser.add_argument('--neuron.sync_interval', default=100, type=int, help='Batches before we sync with chain and emit new weights.') - parser.add_argument('--neuron.log_interval', default=10, type=int, help='Batches before we log session info.') - parser.add_argument('--neuron.accumulation_interval', default=1, type=int, help='Batches before we apply acummulated gradients.') - parser.add_argument('--neuron.apply_remote_gradients', default=False, type=bool, help='If true, neuron applies gradients which accumulate from remotes calls.') - parser.add_argument('--neuron.name', default='gpt-wiki', type=str, help='Trials for this neuron go in neuron.datapath / neuron.name') - parser.add_argument('--neuron.trial_id', default=str(time.time()).split('.')[0], type=str, help='Saved models go in neuron.datapath / neuron.name / neuron.trial_id') - parser.add_argument('--neuron.config_file', default='adam_config.yaml', help='Saved run config') - parser.add_argument('--neuron.record_log', default=True, help='Record all logs when running this session') - GPT2LMSynapse.add_args(parser) - -def check_config(config: Munch): - assert config.neuron.momentum > 0 and config.neuron.momentum < 1, "momentum must be a value between 0 and 1" - assert config.neuron.batch_size_train > 0, "batch_size_train must a positive value" - assert config.neuron.learning_rate > 0, "learning_rate must be a positive value." - trial_path = '{}/{}/{}'.format(config.neuron.datapath, config.neuron.name, config.neuron.trial_id) - config.neuron.trial_path = trial_path - if not os.path.exists(config.neuron.trial_path): - os.makedirs(config.neuron.trial_path) - GPT2LMSynapse.check_config(config) - -# Neuron main. -def main(config: Munch, session: Session): - - # # ---- Model ---- - model = GPT2LMSynapse(config, session) - - # ---- Serve ---- - session.serve( model ) # Serves model to Axon RPC endpoint for network access. - - # ---- Optimizer ---- - optimizer = torch.optim.SGD(model.parameters(), lr = config.neuron.learning_rate, momentum=config.neuron.momentum) - scheduler = WarmupLinearSchedule(optimizer,5,1000) - - # ---- Dataset ---- - # 74 million sentences pulled from books. - dataset = load_dataset('bookcorpus')['train'] - - tensorboard = SummaryWriter(log_dir = config.neuron.trial_path) - - if config.neuron.record_log: - current_time = time.strftime("%H_%M_%S", time.localtime()) - logger.add("{}_{}.log".format(config.neuron.name, current_time),format="{time:YYYY-MM-DD at HH:mm:ss} | {level} | {message}") - - # ---- Train Epoch ---- - def train(epoch: int, global_step: int): - # ----- Init training state --- - model.train() - session.metagraph.sync() # Sync with the chain. - row_weights = session.metagraph.W[ 0, :] # My weights on the chain-state (zeros initially). - history = [] - local_step = 0 - local_epochs = 10 - output = None - net = torch.nn.DataParallel(model) - - while local_step < local_epochs: - try: - inputs = nextbatch(dataset, config.neuron.batch_size_train, bittensor.__tokenizer__) - - output = net( - inputs, - training = True, - remote = True # WITH rpc-queries made to the network - ) - - # ---- Backward pass ---- - output.loss.backward() # Accumulates gradients on the model. - optimizer.step() # Applies accumulated gradients. - optimizer.zero_grad() # Zeros out gradients for next accummulation - - history.append(output) # Save for later analysis/logs. - logger.info('GS: {} LS: {} Epoch: {} \t Local Target Loss: {}\tRemote Target Loss: {}\tDistillation Loss: {}', - colored('{}'.format(global_step), 'red'), - colored('{}'.format(local_step), 'blue'), - colored('{}'.format(epoch), 'green'), - colored('{:.4f}'.format(output.local_target_loss.item()), 'green'), - colored('{:.4f}'.format(output.remote_target_loss.item()), 'blue'), - colored('{:.4f}'.format(output.distillation_loss.item()), 'red'),) - - tensorboard.add_scalar('Rloss', output.remote_target_loss.item(), global_step) - tensorboard.add_scalar('Lloss', output.local_target_loss.item(), global_step) - tensorboard.add_scalar('Dloss', output.distillation_loss.item(), global_step) - - # ---- Update State ---- - batch_weights = torch.mean(output.weights, axis = 0) # Average over batch. - row_weights = (1 - 0.03) * row_weights + 0.03 * batch_weights # Moving avg update. - row_weights = F.normalize(row_weights, p = 1, dim = 0) # Ensure normalization. - - if (global_step + 1) % config.neuron.sync_interval == 0: - # ---- Sync Metagraph State ---- - logger.info('Emitting with weights {}', row_weights.tolist()) - session.metagraph.emit( row_weights, wait_for_inclusion = True ) # Sets my row-weights on the chain. - session.metagraph.sync() # Pulls the latest metagraph state (with my update.) - row_weights = session.metagraph.W[ 0, :] - - # ---- Update Axon Priority ---- - col_weights = session.metagraph.W[:,0] - session.axon.set_priority( session.metagraph.neurons, col_weights ) # Sets the nucleus-backend request priority. - - local_step += 1 - global_step += 1 - torch.cuda.empty_cache() - - # --- Catch Errors during training ---- - except Exception as e: - logger.error('Exception in training script with error: {}', e) - logger.info(traceback.print_exc()) - logger.info('Continuing to train.') - - return output, global_step - - epoch = -1 - global_step = 0 - best_train_loss = math.inf - while True: - epoch += 1 - - # ---- Train Model ---- - output, global_step = train(epoch, global_step) - scheduler.step() - - # Save best loss and model - if output: - train_loss = output.local_target_loss - - if train_loss < best_train_loss: - best_train_loss = train_loss # update best train loss - logger.info( 'Saving/Serving model: epoch: {}, loss: {}, path: {}/model.torch'.format(epoch, best_train_loss, config.neuron.trial_path)) - torch.save( {'epoch': epoch, 'model': model.state_dict(), 'loss': best_train_loss},"{}/model.torch".format(config.neuron.trial_path)) - tensorboard.add_scalar('Train loss', train_loss, global_step) - - -if __name__ == "__main__": - # ---- Load config ---- - parser = argparse.ArgumentParser(); add_args(parser) - config = Config.load(parser); check_config(config) - logger.info(Config.toString(config)) - - # ---- Build Session ---- - session = bittensor.init(config) - - # ---- Start Neuron ---- - with session: - main(config, session) - diff --git a/neurons/bert_mlm.py b/neurons/bert_mlm.py deleted file mode 100755 index 34bb3df24f..0000000000 --- a/neurons/bert_mlm.py +++ /dev/null @@ -1,179 +0,0 @@ -#!/bin/python3 -"""BERT Masked Language Modelling. - -This file demonstrates training the BERT neuron with masked language modelling. - -Example: - $ python neurons/bert_mlm.py - -""" -import bittensor -import argparse -from bittensor.config import Config -from bittensor.utils.logging import log_outputs -from bittensor.subtensor.interface import Keypair -from bittensor.synapses.bert import BertMLMSynapse - -from loguru import logger -from datasets import load_dataset -from bittensor.utils.logging import log_batch_weights, log_chain_weights, log_request_sizes -import random -import time -import torch -import torch.nn.functional as F -from transformers import DataCollatorForLanguageModeling -from munch import Munch -import math -import os - -def add_args(parser: argparse.ArgumentParser): - parser.add_argument('--neuron.datapath', default='data/', type=str, - help='Path to load and save data.') - parser.add_argument('--neuron.learning_rate', default=0.01, type=float, - help='Training initial learning rate.') - parser.add_argument('--neuron.momentum', default=0.98, type=float, - help='Training initial momentum for SGD.') - parser.add_argument('--neuron.batch_size_train', default=20, type=int, - help='Training batch size.') - parser.add_argument('--neuron.batch_size_test', default=20, type=int, - help='Testing batch size.') - parser.add_argument('--neuron.sync_interval', default=100, type=int, - help='Batches before we we sync with chain and emit new weights.') - parser.add_argument('--neuron.name', default='bert_nsp', type=str, help='Trials for this neuron go in neuron.datapath / neuron.name') - parser.add_argument('--neuron.trial_id', default=str(time.time()).split('.')[0], type=str, help='Saved models go in neuron.datapath / neuron.name / neuron.trial_id') - BertMLMSynapse.add_args(parser) - -def check_config(config: Munch): - assert config.neuron.momentum > 0 and config.neuron.momentum < 1, "momentum must be a value between 0 and 1" - assert config.neuron.batch_size_train > 0, "batch_size_train must a positive value" - assert config.neuron.batch_size_test > 0, "batch_size_test must a positive value" - assert config.neuron.learning_rate > 0, "learning_rate must be a positive value." - trial_path = '{}/{}/{}'.format(config.neuron.datapath, config.neuron.name, config.neuron.trial_id) - config.neuron.trial_path = trial_path - if not os.path.exists(config.neuron.trial_path): - os.makedirs(config.neuron.trial_path) - BertMLMSynapse.check_config(config) - -def mlm_batch(data, batch_size, tokenizer, collator): - """ Returns a random batch from text dataset with 50 percent NSP. - - Args: - data: (List[dict{'text': str}]): Dataset of text inputs. - batch_size: size of batch to create. - - Returns: - tensor_batch torch.Tensor (batch_size, sequence_length): List of tokenized sentences. - labels torch.Tensor (batch_size, sequence_length) - """ - batch_text = [] - for _ in range(batch_size): - batch_text.append(data[random.randint(0, len(data))]['text']) - - # Tokenizer returns a dict { 'input_ids': list[], 'attention': list[] } - # but we need to convert to List [ dict ['input_ids': ..., 'attention': ... ]] - # annoying hack... - tokenized = tokenizer(batch_text) - tokenized = [dict(zip(tokenized,t)) for t in zip(*tokenized.values())] - - # Produces the masked language model inputs aw dictionary dict {'inputs': tensor_batch, 'labels': tensor_batch} - # which can be used with the Bert Language model. - collated_batch = collator(tokenized) - return collated_batch['input_ids'], collated_batch['labels'] - -def train(model, config, session, optimizer, scheduler, dataset, collator): - step = 0 - best_loss = math.inf - model.train() # Turn on the train mode. - weights = None - history = [] - batch_idx = 0 - device = torch.device("cuda" if torch.cuda.is_available() else "cpu") - while True: - optimizer.zero_grad() # Clear gradients. - - # Sync with chain. - if batch_idx % config.neuron.sync_interval == 0: - weights = session.metagraph.sync(weights) - weights = weights.to(device) - - # Next batch. - inputs, labels = mlm_batch(dataset, config.neuron.batch_size_train, bittensor.__tokenizer__, collator) - - # Forward pass. - output = model( inputs.to(model.device), labels.to(model.device), remote = True) - history.append(output) - - # Backprop. - output.loss.backward() - optimizer.step() - scheduler.step() - - # Update weights. - batch_weights = F.softmax(torch.mean(output.weights, axis=0), dim=0) # Softmax weights. - weights = (1 - 0.05) * weights + 0.05 * batch_weights # Moving Avg - weights = weights / torch.sum(weights) # Normalize. - - # Log. - step += 1 - logger.info('Step: {} \t Remote Loss: {:.6f}\t Local Loss: {:.6f}\t Distilation Loss: {:.6f}'.format( - step, output.loss.item(), output.remote_target_loss.item(), output.distillation_loss.item())) - log_outputs(history) - log_batch_weights(session, history) - log_chain_weights(session) - log_request_sizes(session, history) - history = [] - - # After each epoch, checkpoint the losses and re-serve the network. - if output.loss.item() < best_loss: - best_loss = output.loss - logger.info( 'Saving/Serving model: epoch: {}, loss: {}, path: {}/{}/{}/model.torch', step, output.loss, config.neuron.datapath, config.neuron.name, config.neuron.trial_id) - torch.save( {'epoch': step, 'model': model.state_dict(), 'loss': output.loss},"{}/{}/{}/model.torch".format(config.neuron.datapath , config.neuron.name, config.neuron.trial_id)) - # Save experiment metrics - session.serve( model.deepcopy() ) - - batch_idx += 1 - - -def main(config, session): - # Build Synapse - model = BertMLMSynapse(config, session) - device = torch.device("cuda" if torch.cuda.is_available() else "cpu") - model.to(device) - session.serve( model ) - - # Dataset: 74 million sentences pulled from books. - # The collator accepts a list [ dict{'input_ids, ...; } ] where the internal dict - # is produced by the tokenizer. - dataset = load_dataset('bookcorpus')['train'] - data_collator = DataCollatorForLanguageModeling( - tokenizer=bittensor.__tokenizer__, mlm=True, mlm_probability=0.15 - ) - - # Optimizer. - optimizer = torch.optim.SGD(model.parameters(), lr = config.neuron.learning_rate) - scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=5, gamma=0.1) - - # Create directory if it does not exist. - data_directory = '{}/{}/{}'.format(config.neuron.datapath, config.neuron.name, config.neuron.trial_id) - if not os.path.exists(data_directory): - os.makedirs(data_directory) - - # train forever. - train(model, config, session, optimizer, scheduler, dataset, data_collator) - - -if __name__ == "__main__": - # Load bittensor config. - parser = argparse.ArgumentParser() - add_args(parser) - config = Config.load(parser) - check_config(config) - logger.info(Config.toString(config)) - - # Load Session. - session = bittensor.init(config) - - # Start Neuron. - with session: - main(config, session) - \ No newline at end of file diff --git a/neurons/bert_nsp.py b/neurons/bert_nsp.py deleted file mode 100755 index 61057a5251..0000000000 --- a/neurons/bert_nsp.py +++ /dev/null @@ -1,183 +0,0 @@ -#!/bin/python3 -"""BERT Next Sentence Prediction Neuron. - -This file demonstrates training the BERT neuron with next sentence prediction. - -Example: - $ python neurons/bert_nsp.py - -""" -import bittensor -import argparse -from bittensor.config import Config -from bittensor.utils.logging import log_outputs -from bittensor.subtensor.interface import Keypair -from bittensor.synapses.bert import BertNSPSynapse - - -from loguru import logger -from datasets import load_dataset -from bittensor.utils.logging import log_batch_weights, log_chain_weights, log_request_sizes -import random -import time -import torch -import torch.nn.functional as F -from munch import Munch -import math -import os - -def nsp_batch(data, batch_size, tokenizer): - """ Returns a random batch from text dataset with 50 percent NSP. - - Args: - data: (List[dict{'text': str}]): Dataset of text inputs. - batch_size: size of batch to create. - - Returns: - input_ids List[str]: List of sentences. - batch_labels torch.Tensor(batch_size): 1 if random next sentence, otherwise 0. - """ - - batch_inputs = [] - batch_next = [] - batch_labels = [] - for _ in range(batch_size): - if random.random() > 0.5: - pos = random.randint(0, len(data)) - batch_inputs.append(data[pos]['text']) - batch_next.append(data[pos + 1]['text']) - batch_labels.append(0) - else: - while True: - pos_1 = random.randint(0, len(data)) - pos_2 = random.randint(0, len(data)) - batch_inputs.append(data[pos_1]['text']) - batch_next.append(data[pos_2]['text']) - batch_labels.append(1) - if (pos_1 != pos_2) and (pos_1 != pos_2 - 1): - break - - tokenized = tokenizer(batch_inputs, text_pair = batch_next, return_tensors='pt', padding=True) - return tokenized, torch.tensor(batch_labels, dtype=torch.long) - -def add_args(parser: argparse.ArgumentParser): - parser.add_argument('--neuron.datapath', default='data/', type=str, - help='Path to load and save data.') - parser.add_argument('--neuron.learning_rate', default=0.01, type=float, - help='Training initial learning rate.') - parser.add_argument('--neuron.momentum', default=0.98, type=float, - help='Training initial momentum for SGD.') - parser.add_argument('--neuron.batch_size_train', default=20, type=int, - help='Training batch size.') - parser.add_argument('--neuron.batch_size_test', default=20, type=int, - help='Testing batch size.') - parser.add_argument('--neuron.sync_interval', default=100, type=int, - help='Batches before we we sync with chain and emit new weights.') - parser.add_argument('--neuron.name', default='bert_nsp', type=str, help='Trials for this neuron go in neuron.datapath / neuron.name') - parser.add_argument('--neuron.trial_id', default=str(time.time()).split('.')[0], type=str, help='Saved models go in neuron.datapath / neuron.name / neuron.trial_id') - BertNSPSynapse.add_args(parser) - -def check_config(config: Munch) -> Munch: - assert config.neuron.momentum > 0 and config.neuron.momentum < 1, "momentum must be a value between 0 and 1" - assert config.neuron.batch_size_train > 0, "batch_size_train must a positive value" - assert config.neuron.batch_size_test > 0, "batch_size_test must a positive value" - assert config.neuron.learning_rate > 0, "learning_rate must be a positive value." - trial_path = '{}/{}/{}'.format(config.neuron.datapath, config.neuron.name, config.neuron.trial_id) - config.neuron.trial_path = trial_path - if not os.path.exists(config.neuron.trial_path): - os.makedirs(config.neuron.trial_path) - BertNSPSynapse.check_config(config) - -def train(model, config, session, optimizer, scheduler, dataset): - step = 0 - best_loss = math.inf - model.train() # Turn on the train mode. - weights = None - history = [] - batch_idx = 0 - device = torch.device("cuda" if torch.cuda.is_available() else "cpu") - while True: - optimizer.zero_grad() # Clear gradients. - - # Sync with chain. - if step % config.neuron.sync_interval == 0: - weights = session.metagraph.sync(weights) - weights = weights.to(device) - - - # Next batch. - inputs, targets = nsp_batch(dataset['train'], config.neuron.batch_size_train, bittensor.__tokenizer__) - - # Forward pass. - output = model (inputs = inputs['input_ids'].to(model.device), - targets = targets.to(model.device), - remote = True ) - history.append(output) - - # Backprop. - output.loss.backward() - optimizer.step() - scheduler.step() - - # Update weights. - batch_weights = F.softmax(torch.mean(output.weights, axis=0), dim=0) # Softmax weights. - weights = (1 - 0.05) * weights + 0.05 * batch_weights # Moving Avg - weights = weights / torch.sum(weights) # Normalize. - - # Log. - step += 1 - logger.info('Step: {} \t Remote Loss: {:.6f}\t Local Loss: {:.6f}\t Distilation Loss: {:.6f}'.format( - step, output.loss.item(), output.remote_target_loss.item(), output.distillation_loss.item())) - log_outputs(history) - log_batch_weights(session, history) - log_chain_weights(session) - log_request_sizes(session, history) - history = [] - - # After each epoch, checkpoint the losses and re-serve the network. - if output.loss.item() < best_loss: - best_loss = output.loss - logger.info( 'Saving/Serving model: epoch: {}, loss: {}, path: {}/{}/{}/model.torch', step, output.loss, config.neuron.datapath, config.neuron.name, config.neuron.trial_id) - torch.save( {'epoch': step, 'model': model.state_dict(), 'loss': output.loss},"{}/{}/{}/model.torch".format(config.neuron.datapath , config.neuron.name, config.neuron.trial_id)) - # Save experiment metrics - session.serve( model.deepcopy() ) - - -def main(config, session): - # Build Synapse - model = BertNSPSynapse(config, session) - device = torch.device("cuda" if torch.cuda.is_available() else "cpu") - model.to(device) - session.serve( model ) - - # Dataset: 74 million sentences pulled from books. - dataset = load_dataset('bookcorpus') - - # Optimizer. - optimizer = torch.optim.SGD(model.parameters(), lr = config.neuron.learning_rate) - scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=5, gamma=0.1) - - # Create directory if it does not exist. - data_directory = '{}/{}/{}'.format(config.neuron.datapath, config.neuron.name, config.neuron.trial_id) - if not os.path.exists(data_directory): - os.makedirs(data_directory) - - # train forever. - train(model, config, session, optimizer, scheduler, dataset) - - -if __name__ == "__main__": - # Load bittensor config. - parser = argparse.ArgumentParser() - add_args(parser) - config = Config.load(parser) - check_config(config) - logger.info(Config.toString(config)) - - # Load Session. - session = bittensor.init(config) - - # Start Neuron. - with session: - main(config, session) - diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000000..c29ce7a344 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,98 @@ +[build-system] +requires = ["setuptools>=70.0.0", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "bittensor" +version = "9.10.1" +description = "Bittensor" +readme = "README.md" +authors = [ + {name = "bittensor.com"} +] +license = { file = "LICENSE" } +requires-python = ">=3.10,<3.14" +dependencies = [ + + "wheel", + "setuptools~=70.0.0", + "aiohttp~=3.9", + "asyncstdlib~=3.13.0", + "colorama~=0.4.6", + "fastapi~=0.110.1", + "munch~=2.5.0", + "numpy>=2.0.1,<3.0.0", + "msgpack-numpy-opentensor~=0.5.0", + "nest_asyncio==1.6.0", + "netaddr==1.3.0", + "packaging", + "python-statemachine~=2.1", + "pycryptodome>=3.18.0,<4.0.0", + "pyyaml>=6.0", + "retry==0.9.2", + "requests>=2.0.0,<3.0", + "pydantic>=2.3, <3", + "scalecodec==1.2.11", + "uvicorn", + "bittensor-drand>=1.0.0,<2.0.0", + "bittensor-wallet>=4.0.0,<5.0", + "async-substrate-interface>=1.5.1" +] + +[project.optional-dependencies] +dev = [ + "pytest==8.3.5", + "pytest-asyncio==0.26.0", + "pytest-mock==3.14.0", + "pytest-split==0.10.0", + "pytest-xdist==3.6.1", + "pytest-rerunfailures==10.2", + "coveralls==3.3.1", + "pytest-cov==4.0.0", + "ddt==1.6.0", + "hypothesis==6.81.1", + "flake8==7.0.0", + "mypy==1.8.0", + "types-retry==0.9.9.4", + "freezegun==1.5.0", + "httpx==0.27.0", + "ruff==0.11.5", + "aioresponses==0.7.6", + "factory-boy==3.3.0", + "types-requests", + "torch>=1.13.1,<3.0" +] +torch = [ + "torch>=1.13.1,<3.0" +] +cli = [ + "bittensor-cli>=9.0.2" +] + + +[project.urls] +# more details can be found here +homepage = "https://github.com/opentensor/bittensor" +Repository = "https://github.com/opentensor/bittensor" + +[tool.flit.metadata] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Topic :: Scientific/Engineering", + "Topic :: Scientific/Engineering :: Mathematics", + "Topic :: Scientific/Engineering :: Artificial Intelligence", + "Topic :: Software Development", + "Topic :: Software Development :: Libraries", + "Topic :: Software Development :: Libraries :: Python Modules" +] + +[tool.setuptools] +package-dir = {"bittensor" = "bittensor"} +script-files = ["bittensor/utils/certifi.sh"] \ No newline at end of file diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 8471d5eedb..0000000000 --- a/requirements.txt +++ /dev/null @@ -1,38 +0,0 @@ -argparse -autobahn==20.7.1 -base58==2.0.1 -coverage -codecov -certifi==2020.6.20 -cryptography -datasets -docker==4.3.1 -idna==2.10 -google-api-python-client -grpcio -grpcio-tools -loguru -munch -netaddr -numpy -pandas -password_strength -pickle-mixin -pycryptodome -pycrypto -py-sr25519-bindings==0.1.2 -py-ed25519-bindings==0.1.1 -py-bip39-bindings>=0.1.6 -pytest-asyncio -pytest>=6.1.2 -pyyaml -replicate -rollbar -scalecodec==0.10.30 -termcolor -tensorboard -torch -torchvision -transformers -validators -xxhash==1.3.0 diff --git a/scratchpad/keys.py b/scratchpad/keys.py deleted file mode 100755 index 4500979089..0000000000 --- a/scratchpad/keys.py +++ /dev/null @@ -1,67 +0,0 @@ -#!/bin/python3 - -from argparse import ArgumentParser -from pathlib import Path -from loguru import logger - -from cryptography.fernet import Fernet -from cryptography.hazmat.primitives import hashes -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC -import base64 - -test_json = """{ - "accountId": "0x96f84a32fe0fd3b5e61b68705340e50a300562a85172b97ec113e76665caaf65", - "publicKey": "0x96f84a32fe0fd3b5e61b68705340e50a300562a85172b97ec113e76665caaf65", - "secretPhrase": "duty embrace auto sketch ring fluid enough resemble insect nuclear top vital congress ship conduct lobster lunch pause clump walk wash stereo force scrap", - "secretSeed": "0xde2c5031d26e80c9b553b94fc7a6ba39b4b83e103689e2bfc425104d6dcaf35c", - "ss58Address": "5FUeoCZBwqh7bNsMB73iDDAQhmWjjzYAMkK1vUP6jXe172yp" -}""" - - - -def generate_key(password): - kdf = PBKDF2HMAC(algorithm=hashes.SHA256(), salt=b"Iguesscyborgslikemyselfhaveatendencytobeparanoidaboutourorigins", length=32, iterations=10, backend=default_backend()) - key = base64.urlsafe_b64encode(kdf.derive(password.encode())) - return key - - -parser = ArgumentParser(description="Key loading test script") -parser.add_argument("--key", required=False, default="~/.bittensor/key", help="Path to the keyfile") - -args = parser.parse_args() -file = Path(args.key) -file = file.expanduser() - -if not file.is_file(): - logger.error("File {} not found.", file.__fspath__()) - - -password = input("Enncryption password ?") -key = generate_key(password) - - - - - -clear = base64.urlsafe_b64decode(cipher_text) -print(clear) - -password = input('Decryption password ?') -key = generate_key(password) - -cipher_suite = Fernet(key) -plaintext = cipher_suite.decrypt(cipher_text) - - -print(plaintext.decode()) - - - - - - - - - - diff --git a/scripts/build_protos.sh b/scripts/build_protos.sh deleted file mode 100755 index dd884ade03..0000000000 --- a/scripts/build_protos.sh +++ /dev/null @@ -1 +0,0 @@ -python3 -m grpc.tools.protoc bittensor/bittensor.proto -I. --python_out=. --grpc_python_out=. diff --git a/scripts/check_pre_submit.sh b/scripts/check_pre_submit.sh new file mode 100755 index 0000000000..8ed3598ebc --- /dev/null +++ b/scripts/check_pre_submit.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +# ruff checks formatting +echo ">>> Run the pre-submit format check with \`ruff format .\`." +ruff format . + +echo ">>> Run the pre-submit format check with \`mypy\`." + +# mypy checks python versions compatibility +versions=("3.10" "3.11", "3.12", "3.13") +for version in "${versions[@]}"; do + echo "Running mypy for Python $version..." + mypy --ignore-missing-imports bittensor/ --python-version="$version" +done + +# flake8 checks errors count in bittensor folder +error_count=$(flake8 bittensor/ --count) +echo ">>> Flake8 found ${error_count} errors." diff --git a/scripts/check_requirements_changes.sh b/scripts/check_requirements_changes.sh new file mode 100755 index 0000000000..4a736f525d --- /dev/null +++ b/scripts/check_requirements_changes.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +# Check if requirements files have changed in the last commit +if git diff --name-only HEAD | grep -E 'pyproject.toml'; then + echo "Requirements files may have changed. Running compatibility checks..." + echo 'export REQUIREMENTS_CHANGED="true"' >> $BASH_ENV +else + echo "Requirements files have not changed. Skipping compatibility checks..." + echo 'export REQUIREMENTS_CHANGED="false"' >> $BASH_ENV +fi diff --git a/scripts/create_wallet.sh b/scripts/create_wallet.sh new file mode 100755 index 0000000000..d0ee08b69f --- /dev/null +++ b/scripts/create_wallet.sh @@ -0,0 +1,13 @@ +mkdir -p ~/.bittensor/wallets/default/hotkeys +rm ~/.bittensor/wallets/default/coldkeypub.txt +rm ~/.bittensor/wallets/default/hotkeys/default +touch ~/.bittensor/wallets/default/coldkeypub.txt +touch ~/.bittensor/wallets/default/hotkeys/default +echo "0x74acaa8d7829336dfff7569f19225818cc593335b9aafcde3f69db23c3538561" >> ~/.bittensor/wallets/default/coldkeypub.txt +echo '{"accountId": "0x9cf7085aa3304c21dc0f571c0134abb12f2e8e1bc9dbfc82440b8d6ba7908655", "publicKey": "0x9cf7085aa3304c21dc0f571c0134abb12f2e8e1bc9dbfc82440b8d6ba7908655", "secretPhrase": "document usage siren cross across crater shrug jump marine distance absurd caught", "secretSeed": "0x2465ae0757117bea271ad622e1cd0c4b319c96896a3c7d9469a68e63cf7f9646", "ss58Address": "5FcWiCiFoSspGGocSxzatNL5kT6cjxjXQ9LuAuYbvFNUqcfX"}' >> ~/.bittensor/wallets/default/hotkeys/default +chmod 0600 ~/.bittensor/wallets/default/coldkeypub.txt +chmod 0600 ~/.bittensor/wallets/default/hotkeys/default +echo "~/.bittensor/wallets/default/coldkeypub.txt" +cat ~/.bittensor/wallets/default/coldkeypub.txt +echo "~/.bittensor/wallets/default/hotkeys/default" +cat ~/.bittensor/wallets/default/hotkeys/default \ No newline at end of file diff --git a/scripts/install.sh b/scripts/install.sh new file mode 100755 index 0000000000..73c5c4945c --- /dev/null +++ b/scripts/install.sh @@ -0,0 +1,311 @@ +#!/bin/bash +set -u + +# enable command completion +set -o history -o histexpand + +python="python3" + +abort() { + printf "%s\n" "$1" + exit 1 +} + +getc() { + local save_state + save_state=$(/bin/stty -g) + /bin/stty raw -echo + IFS= read -r -n 1 -d '' "$@" + /bin/stty "$save_state" +} + +exit_on_error() { + exit_code=$1 + last_command=${@:2} + if [ $exit_code -ne 0 ]; then + >&2 echo "\"${last_command}\" command failed with exit code ${exit_code}." + exit $exit_code + fi +} + +wait_for_user() { + local c + echo + echo "Press RETURN to continue or any other key to abort" + getc c + if ! [[ "$c" == $'\r' || "$c" == $'\n' ]]; then + exit 1 + fi +} + +shell_join() { + local arg + printf "%s" "$1" + shift + for arg in "$@"; do + printf " " + printf "%s" "${arg// /\ }" + done +} + +# string formatters +if [[ -t 1 ]]; then + tty_escape() { printf "\033[%sm" "$1"; } +else + tty_escape() { :; } +fi +tty_mkbold() { tty_escape "1;$1"; } +tty_underline="$(tty_escape "4;39")" +tty_blue="$(tty_mkbold 34)" +tty_red="$(tty_mkbold 31)" +tty_bold="$(tty_mkbold 39)" +tty_reset="$(tty_escape 0)" + +ohai() { + printf "${tty_blue}==>${tty_bold} %s${tty_reset}\n" "$(shell_join "$@")" +} + +linux_install_pre() { + sudo apt-get update + sudo apt-get install --no-install-recommends --no-install-suggests -y apt-utils curl git cmake build-essential + exit_on_error $? +} + +linux_install_python() { + which $python + if [[ $? != 0 ]] ; then + ohai "Installing python" + sudo apt-get install --no-install-recommends --no-install-suggests -y $python + else + ohai "Updating python" + sudo apt-get install --only-upgrade $python + fi + exit_on_error $? + ohai "Installing python tools" + sudo apt-get install --no-install-recommends --no-install-suggests -y python3-pip python3-dev python3-venv + exit_on_error $? +} + +linux_update_pip() { + ohai "Skipping pip upgrade in system Python (PEP 668). Will upgrade inside virtual environment." +} + + +linux_install_bittensor() { + ohai "Cloning bittensor@master into ~/.bittensor/bittensor" + mkdir -p ~/.bittensor/bittensor + git clone https://github.com/opentensor/bittensor.git ~/.bittensor/bittensor/ 2> /dev/null || \ + (cd ~/.bittensor/bittensor/ ; git fetch origin master ; git checkout master ; git pull --ff-only ; git reset --hard ; git clean -xdf) + + ohai "Creating Python virtual environment" + python3 -m venv ~/.bittensor/venv + $HOME/.bittensor/venv/bin/python -m ensurepip --upgrade + source ~/.bittensor/venv/bin/activate + python="$HOME/.bittensor/venv/bin/python" + + ohai "Installing bittensor" + $python -m pip install --upgrade pip + $python -m pip install -e ~/.bittensor/bittensor/ + $python -m pip install -U bittensor-cli + exit_on_error $? + deactivate +} + +linux_increase_ulimit(){ + ohai "Increasing ulimit to 1,000,000" + prlimit --pid=$PPID --nofile=1000000 +} + +mac_install_xcode() { + which -s xcode-select + if [[ $? != 0 ]] ; then + ohai "Installing xcode:" + xcode-select --install + exit_on_error $? + fi +} + +mac_install_brew() { + which -s brew + if [[ $? != 0 ]] ; then + ohai "Installing brew:" + ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" + else + ohai "Updating brew:" + brew update --verbose + fi + exit_on_error $? +} + +mac_install_cmake() { + which -s cmake + if [[ $? != 0 ]] ; then + ohai "Installing cmake:" + brew install cmake + else + ohai "Updating cmake:" + brew upgrade cmake + fi +} + +mac_install_python() { + which -s python3 + ohai "Installing python3" + brew list python@3 &>/dev/null || brew install python@3; + ohai "Updating python3" + brew upgrade python@3 + exit_on_error $? +} + +mac_update_pip() { + PYTHONPATH=$(which $python) + ohai "You are using python@ $PYTHONPATH$" + ohai "Installing python tools" + $python -m pip install --upgrade pip +} + +mac_install_bittensor() { + ohai "Cloning bittensor into ~/.bittensor/bittensor" + git clone https://github.com/opentensor/bittensor.git ~/.bittensor/bittensor/ 2> /dev/null || \ + (cd ~/.bittensor/bittensor/ ; git fetch origin master ; git checkout master ; git pull --ff-only ; git reset --hard; git clean -xdf) + + ohai "Creating Python virtual environment" + python3 -m venv ~/.bittensor/venv + $HOME/.bittensor/venv/bin/python -m ensurepip --upgrade + source ~/.bittensor/venv/bin/activate + python="$HOME/.bittensor/venv/bin/python" + + ohai "Installing bittensor" + $python -m pip install --upgrade pip + $python -m pip install -e ~/.bittensor/bittensor/ + $python -m pip install -U bittensor-cli + exit_on_error $? + deactivate +} + +# Do install. +OS="$(uname)" +if [[ "$OS" == "Linux" ]]; then + + which -s apt-get + if [[ $? != 0 ]] ; then + abort "This linux based install requires apt-get. To run with other distros (centos, arch, etc), you will need to manually install the requirements" + fi + + echo """ + +██████╗░██╗████████╗████████╗███████╗███╗░░██╗░██████╗░█████╗░██████╗░ +██╔══██╗██║╚══██╔══╝╚══██╔══╝██╔════╝████╗░██║██╔════╝██╔══██╗██╔══██╗ +██████╦╝██║░░░██║░░░░░░██║░░░█████╗░░██╔██╗██║╚█████╗░██║░░██║██████╔╝ +██╔══██╗██║░░░██║░░░░░░██║░░░██╔══╝░░██║╚████║░╚═══██╗██║░░██║██╔══██╗ +██████╦╝██║░░░██║░░░░░░██║░░░███████╗██║░╚███║██████╔╝╚█████╔╝██║░░██║ +╚═════╝░╚═╝░░░╚═╝░░░░░░╚═╝░░░╚══════╝╚═╝░░╚══╝╚═════╝░░╚════╝░╚═╝░░╚═╝ + + - Mining a new element. + """ + ohai "This script will install:" + echo "git" + echo "curl" + echo "cmake" + echo "build-essential" + echo "python3" + echo "python3-pip" + echo "bittensor" + + wait_for_user + linux_install_pre + linux_install_python + linux_update_pip + linux_install_bittensor + + ohai "Would you like to increase the ulimit? This will allow your miner to run for a longer time" + wait_for_user + linux_increase_ulimit + echo "" + echo "" + echo "######################################################################" + echo "## ##" + echo "## BITTENSOR SETUP ##" + echo "## ##" + echo "######################################################################" + echo "" + echo "" + +elif [[ "$OS" == "Darwin" ]]; then + echo """ + +██████╗░██╗████████╗████████╗███████╗███╗░░██╗░██████╗░█████╗░██████╗░ +██╔══██╗██║╚══██╔══╝╚══██╔══╝██╔════╝████╗░██║██╔════╝██╔══██╗██╔══██╗ +██████╦╝██║░░░██║░░░░░░██║░░░█████╗░░██╔██╗██║╚█████╗░██║░░██║██████╔╝ +██╔══██╗██║░░░██║░░░░░░██║░░░██╔══╝░░██║╚████║░╚═══██╗██║░░██║██╔══██╗ +██████╦╝██║░░░██║░░░░░░██║░░░███████╗██║░╚███║██████╔╝╚█████╔╝██║░░██║ +╚═════╝░╚═╝░░░╚═╝░░░░░░╚═╝░░░╚══════╝╚═╝░░╚══╝╚═════╝░░╚════╝░╚═╝░░╚═╝ + + - Mining a new element. + """ + ohai "This script will install:" + echo "xcode" + echo "homebrew" + echo "git" + echo "cmake" + echo "python3" + echo "python3-pip" + echo "bittensor" + + wait_for_user + mac_install_brew + mac_install_cmake + mac_install_python + mac_update_pip + mac_install_bittensor + echo "" + echo "" + echo "######################################################################" + echo "## ##" + echo "## BITTENSOR SETUP ##" + echo "## ##" + echo "######################################################################" +else + abort "Bittensor is only supported on macOS and Linux" +fi + +# Use the shell's audible bell. +if [[ -t 1 ]]; then + printf "\a" +fi + +echo "" +echo "" +ohai "Welcome. Installation successful" +echo "" +echo "- 1. Create a wallet " +echo " $ btcli w new_coldkey # (for holding funds)" +echo " $ btcli w new_hotkey # (for running miners)" +echo "" +echo "- 2. Run a miner on the prompting network. " +echo " $ python3 ~/.bittensor/bittensor/neurons/text/prompting/miners/gpt4all/neuron.py" +echo "" +ohai "Extras:" +echo "" +echo "- Check your tao balance: " +echo " $ btcli w overview" +echo "" +echo "- Stake to your miners:" +echo " $ btcli stake add" +echo " $ btcli stake remove" +echo "" +echo "- Create/list/register wallets" +echo " $ btcli w new_coldkey" +echo " $ btcli w new_hotkey" +echo " $ btcli w list" +echo " $ btcli s register" +echo "" +echo "- Check Bittensor SDK version" +echo " $ python -m bittensor" +echo "" +echo "- Use the Python API" +echo " $ python3"echo " >> import bittensor" +echo "" +echo "- Join the discussion:" +echo " ${tty_underline}https://discord.gg/3rUr6EcvbB${tty_reset}" +echo "" diff --git a/scripts/post_install_cli.py b/scripts/post_install_cli.py new file mode 100644 index 0000000000..bfaca34c37 --- /dev/null +++ b/scripts/post_install_cli.py @@ -0,0 +1,29 @@ +import os +import subprocess +import sys + + +def post_install(): + # Determine the shell type (bash, zsh, etc.) + shell = os.environ.get("SHELL") + if "bash" in shell: + shell_config = "~/.bashrc" + elif "zsh" in shell: + shell_config = "~/.zshrc" + else: + print("Unsupported shell for autocompletion.") + return + + # Generate the completion script + completion_script = subprocess.check_output( + [sys.executable, "-m", "bittensor.cli", "--print-completion", shell] + ).decode() + + # Append the completion script to the shell configuration file + with open(os.path.expanduser(shell_config), "a") as file: + file.write("\n# Bittensor CLI Autocompletion\n") + file.write(completion_script) + + +if __name__ == "__main__": + post_install() diff --git a/scripts/push.sh b/scripts/push.sh deleted file mode 100755 index 8aaf1d403a..0000000000 --- a/scripts/push.sh +++ /dev/null @@ -1,3 +0,0 @@ -rm dist/ -python3 setup.py sdist bdist_wheel -python3 -m twine upload --repository testpypi dist/* diff --git a/scripts/push_image.sh b/scripts/push_image.sh deleted file mode 100755 index 9cdd8a39fa..0000000000 --- a/scripts/push_image.sh +++ /dev/null @@ -1,3 +0,0 @@ -./scripts/build_protos.sh -docker build -t bittensor/bittensor:latest -f Dockerfile.base . -docker push bittensor/bittensor:latest \ No newline at end of file diff --git a/setup.py b/setup.py deleted file mode 100644 index eda445240a..0000000000 --- a/setup.py +++ /dev/null @@ -1,52 +0,0 @@ -from setuptools import setup, find_packages -from pkg_resources import parse_requirements -from os import path -from io import open -import codecs -import re -import os - -here = path.abspath(path.dirname(__file__)) - -with open(path.join(here, 'README.md'), encoding='utf-8') as f: - long_description = f.read() - -with open('requirements.txt') as requirements_file: - install_requires = [str(requirement) for requirement in parse_requirements(requirements_file)] - -# loading version from setup.py -with codecs.open(os.path.join(here, 'bittensor/__init__.py'), encoding='utf-8') as init_file: - version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", init_file.read(), re.M) - version_string = version_match.group(1) - -setup( - name='bittensor', - version=version_string, - description='BitTensor', - long_description=long_description, - long_description_content_type='text/markdown', - url='https://github.com/bittensor/bittensor', - author='bittensor.com', - packages=['bittensor'], - author_email='', - license='MIT', - install_requires=install_requires, - classifiers=[ - 'Development Status :: 3 - Alpha', - 'Intended Audience :: Developers', - 'Topic :: Software Development :: Build Tools', - - # Pick your license as you wish - 'License :: OSI Approved :: MIT License', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Topic :: Scientific/Engineering', - 'Topic :: Scientific/Engineering :: Mathematics', - 'Topic :: Scientific/Engineering :: Artificial Intelligence', - 'Topic :: Software Development', - 'Topic :: Software Development :: Libraries', - 'Topic :: Software Development :: Libraries :: Python Modules', - ], - python_requires='>=3.5', -) diff --git a/tests/integration_tests/gpu_tests/__init__.py b/tests/__init__.py similarity index 100% rename from tests/integration_tests/gpu_tests/__init__.py rename to tests/__init__.py diff --git a/tests/integration_tests/gpu_tests/bittensor/__init__.py b/tests/e2e_tests/__init__.py similarity index 100% rename from tests/integration_tests/gpu_tests/bittensor/__init__.py rename to tests/e2e_tests/__init__.py diff --git a/tests/e2e_tests/conftest.py b/tests/e2e_tests/conftest.py new file mode 100644 index 0000000000..1c2f8a42e3 --- /dev/null +++ b/tests/e2e_tests/conftest.py @@ -0,0 +1,317 @@ +import os +import re +import shlex +import shutil +import signal +import subprocess +import sys +import threading +import time + +import pytest +import pytest_asyncio +from async_substrate_interface import SubstrateInterface + +from bittensor.core.subtensor_api import SubtensorApi +from bittensor.utils.btlogging import logging +from tests.e2e_tests.utils.e2e_test_utils import ( + Templates, + setup_wallet, +) + +LOCALNET_IMAGE_NAME = ( + os.getenv("LOCALNET_IMAGE_NAME") + or "ghcr.io/opentensor/subtensor-localnet:devnet-ready" +) +CONTAINER_NAME_PREFIX = "test_local_chain_" + + +def wait_for_node_start(process, timestamp=None): + """Waits for node to start in the docker.""" + while True: + line = process.stdout.readline() + if not line: + break + + timestamp = timestamp or int(time.time()) + print(line.strip()) + # 10 min as timeout + if int(time.time()) - timestamp > 20 * 30: + print("Subtensor not started in time") + raise TimeoutError + + pattern = re.compile(r"Imported #1") + if pattern.search(line): + print("Node started!") + break + + # Start a background reader after pattern is found + # To prevent the buffer filling up + def read_output(): + while True: + if not process.stdout.readline(): + break + + reader_thread = threading.Thread(target=read_output, daemon=True) + reader_thread.start() + + +@pytest.fixture(scope="function") +def local_chain(request): + """Determines whether to run the localnet.sh script in a subprocess or a Docker container.""" + args = request.param if hasattr(request, "param") else None + + # passed env variable to control node mod (non-/fast-blocks) + fast_blocks = "False" if (os.getenv("FAST_BLOCKS") == "0") is True else "True" + params = f"{fast_blocks}" if args is None else f"{fast_blocks} {args} " + + if shutil.which("docker") and not os.getenv("USE_DOCKER") == "0": + yield from docker_runner(params) + else: + if not os.getenv("USE_DOCKER") == "0": + if sys.platform.startswith("linux"): + docker_command = ( + "Install docker with command " + "[blue]sudo apt-get update && sudo apt-get install docker.io -y[/blue]" + " or use documentation [blue]https://docs.docker.com/engine/install/[/blue]" + ) + elif sys.platform == "darwin": + docker_command = ( + "Install docker with command [blue]brew install docker[/blue]" + ) + else: + docker_command = "[blue]Unknown OS, install Docker manually: https://docs.docker.com/get-docker/[/blue]" + + logging.warning("Docker not found in the operating system!") + logging.warning(docker_command) + logging.warning("Tests are run in legacy mode.") + yield from legacy_runner(params) + + +def legacy_runner(params): + """Runs the localnet.sh script in a subprocess and waits for it to start.""" + # Get the environment variable for the script path + script_path = os.getenv("LOCALNET_SH_PATH") + + if not script_path: + # Skip the test if the localhost.sh path is not set + logging.warning("LOCALNET_SH_PATH env variable is not set, e2e test skipped.") + pytest.skip("LOCALNET_SH_PATH environment variable is not set.") + + # Check if param is None and handle it accordingly + args = "" if params is None else f"{params}" + + # Compile commands to send to process + cmds = shlex.split(f"{script_path} {args}") + with subprocess.Popen( + cmds, + start_new_session=True, + stderr=subprocess.STDOUT, + stdout=subprocess.PIPE, + text=True, + ) as process: + try: + wait_for_node_start(process) + except TimeoutError: + raise + else: + with SubstrateInterface(url="ws://127.0.0.1:9944") as substrate: + yield substrate + finally: + # Terminate the process group (includes all child processes) + os.killpg(os.getpgid(process.pid), signal.SIGTERM) + + try: + process.wait(1) + except subprocess.TimeoutExpired: + # If the process is not terminated, send SIGKILL + os.killpg(os.getpgid(process.pid), signal.SIGKILL) + process.wait() + + +def docker_runner(params): + """Starts a Docker container before tests and gracefully terminates it after.""" + + def is_docker_running(): + """Check if Docker is running and optionally skip pulling the image.""" + try: + subprocess.run( + ["docker", "info"], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + check=True, + ) + + skip_pull = os.getenv("SKIP_PULL", "0") == "1" + if not skip_pull: + subprocess.run(["docker", "pull", LOCALNET_IMAGE_NAME], check=True) + else: + print(f"[SKIP_PULL=1] Skipping 'docker pull {LOCALNET_IMAGE_NAME}'") + + return True + except subprocess.CalledProcessError: + return False + + def try_start_docker(): + """Run docker based on OS.""" + try: + subprocess.run(["open", "-a", "Docker"], check=True) # macOS + except (FileNotFoundError, subprocess.CalledProcessError): + try: + subprocess.run(["systemctl", "start", "docker"], check=True) # Linux + except (FileNotFoundError, subprocess.CalledProcessError): + try: + subprocess.run( + ["sudo", "service", "docker", "start"], check=True + ) # Linux alternative + except (FileNotFoundError, subprocess.CalledProcessError): + print("Failed to start Docker. Manual start may be required.") + return False + + # Wait Docker run 10 attempts with 3 sec waits + for _ in range(10): + if is_docker_running(): + return True + time.sleep(3) + + print("Docker wasn't run. Manual start may be required.") + return False + + def stop_existing_test_containers(): + """Stop running Docker containers with names starting with 'test_local_chain_'.""" + try: + existing_container_result = subprocess.run( + [ + "docker", + "ps", + "--filter", + f"name={CONTAINER_NAME_PREFIX}", + "--format", + "{{.ID}}", + ], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + check=True, + ) + container_ids = existing_container_result.stdout.strip().splitlines() + for cid in container_ids: + if cid: + print(f"Stopping existing container: {cid}") + subprocess.run(["docker", "stop", cid], check=True) + except subprocess.CalledProcessError as e: + print(f"Failed to stop existing containers: {e}") + + container_name = f"{CONTAINER_NAME_PREFIX}{str(time.time()).replace('.', '_')}" + + # Command to start container + cmds = [ + "docker", + "run", + "--rm", + "--name", + container_name, + "-p", + "9944:9944", + "-p", + "9945:9945", + str(LOCALNET_IMAGE_NAME), + ] + + cmds += params.split() if params else [] + + print("Entire run command: ", cmds) + + try_start_docker() + + stop_existing_test_containers() + + # Start container + with subprocess.Popen( + cmds, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + start_new_session=True, + ) as process: + try: + try: + wait_for_node_start(process, timestamp=int(time.time())) + except TimeoutError: + raise + + result = subprocess.run( + ["docker", "ps", "-q", "-f", f"name={container_name}"], + capture_output=True, + text=True, + ) + if not result.stdout.strip(): + raise RuntimeError("Docker container failed to start.") + + with SubstrateInterface(url="ws://127.0.0.1:9944") as substrate: + yield substrate + + finally: + try: + subprocess.run(["docker", "kill", container_name]) + process.wait() + except subprocess.TimeoutExpired: + os.killpg(os.getpgid(process.pid), signal.SIGKILL) + + +@pytest.fixture(scope="session") +def templates(): + with Templates() as templates: + yield templates + + +@pytest.fixture +def subtensor(local_chain): + return SubtensorApi(network="ws://localhost:9944", legacy_methods=False) + + +# @pytest.fixture +# def async_subtensor(local_chain): +# a_sub = SubtensorApi( +# network="ws://localhost:9944", legacy_methods=False, async_subtensor=True +# ) +# a_sub.initialize() +# return a_sub + + +@pytest_asyncio.fixture +async def async_subtensor(local_chain): + async with SubtensorApi( + network="ws://localhost:9944", legacy_methods=False, async_subtensor=True + ) as a_sub: + return a_sub + + +@pytest.fixture +def alice_wallet(): + keypair, wallet = setup_wallet("//Alice") + return wallet + + +@pytest.fixture +def bob_wallet(): + keypair, wallet = setup_wallet("//Bob") + return wallet + + +@pytest.fixture +def charlie_wallet(): + keypair, wallet = setup_wallet("//Charlie") + return wallet + + +@pytest.fixture +def dave_wallet(): + keypair, wallet = setup_wallet("//Dave") + return wallet + + +@pytest.fixture +def eve_wallet(): + keypair, wallet = setup_wallet("//Eve") + return wallet diff --git a/tests/e2e_tests/test_axon.py b/tests/e2e_tests/test_axon.py new file mode 100644 index 0000000000..61dff37431 --- /dev/null +++ b/tests/e2e_tests/test_axon.py @@ -0,0 +1,166 @@ +import asyncio + +import pytest + +from bittensor import logging +from bittensor.utils import networking + + +@pytest.mark.asyncio +async def test_axon(subtensor, templates, alice_wallet): + """ + Test the Axon mechanism and successful registration on the network with sync Subtensor. + + Steps: + 1. Register a subnet and register Alice + 2. Check if metagraph.axon is updated and check axon attributes + 3. Run Alice as a miner on subnet + 4. Check the metagraph again after running the miner and verify all attributes + Raises: + AssertionError: If any of the checks or verifications fail + """ + + logging.console.info("Testing test_axon") + + netuid = 2 + + # Register a subnet, netuid 2 + assert subtensor.subnets.register_subnet(alice_wallet), "Subnet wasn't created" + + # Verify subnet created successfully + assert subtensor.subnets.subnet_exists(netuid), "Subnet wasn't created successfully" + + metagraph = subtensor.metagraphs.metagraph(netuid) + + # Validate current metagraph stats + old_axon = metagraph.axons[0] + assert len(metagraph.axons) == 1, f"Expected 1 axon, but got {len(metagraph.axons)}" + assert old_axon.hotkey == alice_wallet.hotkey.ss58_address, ( + "Hotkey mismatch for the axon" + ) + assert old_axon.coldkey == alice_wallet.coldkey.ss58_address, ( + "Coldkey mismatch for the axon" + ) + assert old_axon.ip == "0.0.0.0", f"Expected IP 0.0.0.0, but got {old_axon.ip}" + assert old_axon.port == 0, f"Expected port 0, but got {old_axon.port}" + assert old_axon.ip_type == 0, f"Expected IP type 0, but got {old_axon.ip_type}" + + async with templates.miner(alice_wallet, netuid): + # Waiting for 5 seconds for metagraph to be updated + await asyncio.sleep(5) + + # Refresh the metagraph + metagraph = subtensor.metagraphs.metagraph(netuid) + updated_axon = metagraph.axons[0] + external_ip = networking.get_external_ip() + + # Assert updated attributes + assert len(metagraph.axons) == 1, ( + f"Expected 1 axon, but got {len(metagraph.axons)} after mining" + ) + + assert len(metagraph.neurons) == 1, ( + f"Expected 1 neuron, but got {len(metagraph.neurons)}" + ) + + assert updated_axon.ip == external_ip, ( + f"Expected IP {external_ip}, but got {updated_axon.ip}" + ) + + assert updated_axon.ip_type == networking.ip_version(external_ip), ( + f"Expected IP type {networking.ip_version(external_ip)}, but got {updated_axon.ip_type}" + ) + + assert updated_axon.port == 8091, f"Expected port 8091, but got {updated_axon.port}" + + assert updated_axon.hotkey == alice_wallet.hotkey.ss58_address, ( + "Hotkey mismatch after mining" + ) + + assert updated_axon.coldkey == alice_wallet.coldkey.ss58_address, ( + "Coldkey mismatch after mining" + ) + + logging.console.success("✅ Passed test_axon") + + +@pytest.mark.asyncio +async def test_axon_async(async_subtensor, templates, alice_wallet): + """ + Test the Axon mechanism and successful registration on the network with async Subtensor. + + Steps: + 1. Register a subnet and register Alice + 2. Check if metagraph.axon is updated and check axon attributes + 3. Run Alice as a miner on subnet + 4. Check the metagraph again after running the miner and verify all attributes + Raises: + AssertionError: If any of the checks or verifications fail + """ + + logging.console.info("Testing test_axon") + + netuid = 2 + + # Register a subnet, netuid 2 + assert await async_subtensor.subnets.register_subnet(alice_wallet), ( + "Subnet wasn't created" + ) + + # Verify subnet created successfully + assert await async_subtensor.subnets.subnet_exists(netuid), ( + "Subnet wasn't created successfully" + ) + + metagraph = await async_subtensor.metagraphs.metagraph(netuid) + + # Validate current metagraph stats + old_axon = metagraph.axons[0] + assert len(metagraph.axons) == 1, f"Expected 1 axon, but got {len(metagraph.axons)}" + assert old_axon.hotkey == alice_wallet.hotkey.ss58_address, ( + "Hotkey mismatch for the axon" + ) + assert old_axon.coldkey == alice_wallet.coldkey.ss58_address, ( + "Coldkey mismatch for the axon" + ) + assert old_axon.ip == "0.0.0.0", f"Expected IP 0.0.0.0, but got {old_axon.ip}" + assert old_axon.port == 0, f"Expected port 0, but got {old_axon.port}" + assert old_axon.ip_type == 0, f"Expected IP type 0, but got {old_axon.ip_type}" + + async with templates.miner(alice_wallet, netuid): + # Waiting for 5 seconds for metagraph to be updated + await asyncio.sleep(5) + + # Refresh the metagraph + metagraph = await async_subtensor.metagraphs.metagraph(netuid) + updated_axon = metagraph.axons[0] + external_ip = networking.get_external_ip() + + # Assert updated attributes + assert len(metagraph.axons) == 1, ( + f"Expected 1 axon, but got {len(metagraph.axons)} after mining" + ) + + assert len(metagraph.neurons) == 1, ( + f"Expected 1 neuron, but got {len(metagraph.neurons)}" + ) + + assert updated_axon.ip == external_ip, ( + f"Expected IP {external_ip}, but got {updated_axon.ip}" + ) + + assert updated_axon.ip_type == networking.ip_version(external_ip), ( + f"Expected IP type {networking.ip_version(external_ip)}, but got {updated_axon.ip_type}" + ) + + assert updated_axon.port == 8091, f"Expected port 8091, but got {updated_axon.port}" + + assert updated_axon.hotkey == alice_wallet.hotkey.ss58_address, ( + "Hotkey mismatch after mining" + ) + + assert updated_axon.coldkey == alice_wallet.coldkey.ss58_address, ( + "Coldkey mismatch after mining" + ) + + logging.console.success("✅ Passed test_axon_async") diff --git a/tests/e2e_tests/test_commit_reveal.py b/tests/e2e_tests/test_commit_reveal.py new file mode 100644 index 0000000000..0fc4637359 --- /dev/null +++ b/tests/e2e_tests/test_commit_reveal.py @@ -0,0 +1,465 @@ +import re + +import numpy as np +import pytest + +from bittensor.utils.btlogging import logging +from bittensor.utils.weight_utils import convert_weights_and_uids_for_emit +from tests.e2e_tests.utils.chain_interactions import ( + async_wait_interval, + async_sudo_set_admin_utils, + async_sudo_set_hyperparameter_bool, + sudo_set_admin_utils, + sudo_set_hyperparameter_bool, + wait_interval, + next_tempo, +) + + +# @pytest.mark.parametrize("local_chain", [True], indirect=True) +@pytest.mark.asyncio +async def test_commit_and_reveal_weights_cr4(local_chain, subtensor, alice_wallet): + """ + Tests the commit/reveal weights mechanism (CR3) + + Steps: + 1. Register a subnet through Alice + 2. Register Alice's neuron and add stake + 3. Enable a commit-reveal mechanism on subnet + 4. Lower weights rate limit + 5. Change the tempo for subnet 1 + 5. Commit weights and ensure they are committed. + 6. Wait interval & reveal weights and verify + Raises: + AssertionError: If any of the checks or verifications fail + """ + logging.console.info("Testing `test_commit_and_reveal_weights_cr4`") + + # 12 for non-fast-block, 0.25 for fast block + BLOCK_TIME, TEMPO_TO_SET = ( + (0.25, 100) if subtensor.chain.is_fast_blocks() else (12.0, 20) + ) + + logging.console.info(f"Using block time: {BLOCK_TIME}") + + alice_subnet_netuid = subtensor.subnets.get_total_subnets() # 2 + + # Register root as Alice + assert subtensor.extrinsics.register_subnet(alice_wallet), ( + "Unable to register the subnet" + ) + + # Verify subnet 2 created successfully + assert subtensor.subnets.subnet_exists(alice_subnet_netuid), ( + f"SN #{alice_subnet_netuid} wasn't created successfully" + ) + + logging.console.success(f"SN #{alice_subnet_netuid} is registered.") + + # Enable commit_reveal on the subnet + assert sudo_set_hyperparameter_bool( + substrate=local_chain, + wallet=alice_wallet, + call_function="sudo_set_commit_reveal_weights_enabled", + value=True, + netuid=alice_subnet_netuid, + ), f"Unable to enable commit reveal on the SN #{alice_subnet_netuid}" + + # Verify commit_reveal was enabled + assert subtensor.subnets.commit_reveal_enabled(alice_subnet_netuid), ( + "Failed to enable commit/reveal" + ) + logging.console.success("Commit reveal enabled") + + cr_version = subtensor.substrate.query( + module="SubtensorModule", storage_function="CommitRevealWeightsVersion" + ) + assert cr_version == 4, f"Commit reveal version is not 3, got {cr_version}" + + # Change the weights rate limit on the subnet + status, error = sudo_set_admin_utils( + substrate=local_chain, + wallet=alice_wallet, + call_function="sudo_set_weights_set_rate_limit", + call_params={"netuid": alice_subnet_netuid, "weights_set_rate_limit": "0"}, + ) + + assert status is True + assert error is None + + # Verify weights rate limit was changed + assert ( + subtensor.subnets.get_subnet_hyperparameters( + netuid=alice_subnet_netuid + ).weights_rate_limit + == 0 + ), "Failed to set weights_rate_limit" + assert subtensor.subnets.weights_rate_limit(netuid=alice_subnet_netuid) == 0 + logging.console.success("sudo_set_weights_set_rate_limit executed: set to 0") + + # Change the tempo of the subnet + assert ( + sudo_set_admin_utils( + local_chain, + alice_wallet, + call_function="sudo_set_tempo", + call_params={"netuid": alice_subnet_netuid, "tempo": TEMPO_TO_SET}, + )[0] + is True + ) + + tempo = subtensor.subnets.get_subnet_hyperparameters( + netuid=alice_subnet_netuid + ).tempo + assert tempo == TEMPO_TO_SET, "SN tempos has not been changed." + logging.console.success(f"SN #{alice_subnet_netuid} tempo set to {TEMPO_TO_SET}") + + # Commit-reveal values - setting weights to self + uids = np.array([0], dtype=np.int64) + weights = np.array([0.1], dtype=np.float32) + + weight_uids, weight_vals = convert_weights_and_uids_for_emit( + uids=uids, weights=weights + ) + logging.console.info( + f"Committing weights: uids {weight_uids}, weights {weight_vals}" + ) + + # Fetch current block and calculate next tempo for the subnet + current_block = subtensor.chain.get_current_block() + upcoming_tempo = next_tempo(current_block, tempo) + logging.console.info( + f"Checking if window is too low with Current block: {current_block}, next tempo: {upcoming_tempo}" + ) + + # Lower than this might mean weights will get revealed before we can check them + if upcoming_tempo - current_block < 6: + await wait_interval( + tempo, + subtensor, + netuid=alice_subnet_netuid, + reporting_interval=1, + ) + current_block = subtensor.chain.get_current_block() + latest_drand_round = subtensor.chain.last_drand_round() + upcoming_tempo = next_tempo(current_block, tempo) + logging.console.info( + f"Post first wait_interval (to ensure window isn't too low): {current_block}, next tempo: {upcoming_tempo}, drand: {latest_drand_round}" + ) + + # commit_block is the block when weights were committed on the chain (transaction block) + expected_commit_block = subtensor.block + 1 + # Commit weights + success, message = subtensor.extrinsics.set_weights( + wallet=alice_wallet, + netuid=alice_subnet_netuid, + uids=weight_uids, + weights=weight_vals, + wait_for_inclusion=True, + wait_for_finalization=True, + block_time=BLOCK_TIME, + period=16, + ) + + # Assert committing was a success + assert success is True, message + assert bool(re.match(r"reveal_round:\d+", message)) + + # Parse expected reveal_round + expected_reveal_round = int(message.split(":")[1]) + logging.console.success( + f"Successfully set weights: uids {weight_uids}, weights {weight_vals}, reveal_round: {expected_reveal_round}" + ) + + # Fetch current commits pending on the chain + commits_on_chain = subtensor.commitments.get_timelocked_weight_commits( + netuid=alice_subnet_netuid + ) + address, commit_block, commit, reveal_round = commits_on_chain[0] + + # Assert correct values are committed on the chain + assert expected_reveal_round == reveal_round + assert address == alice_wallet.hotkey.ss58_address + + # bc of the drand delay, the commit block can be either the previous block or the current block + assert expected_commit_block in [commit_block - 1, commit_block, commit_block + 1] + + # Ensure no weights are available as of now + assert subtensor.subnets.weights(netuid=alice_subnet_netuid) == [] + logging.console.success("No weights are available before next epoch.") + + # 5 is safety drand offset + expected_reveal_block = ( + subtensor.subnets.get_next_epoch_start_block(alice_subnet_netuid) + 5 + ) + + logging.console.info( + f"Waiting for the next epoch to ensure weights are revealed: block {expected_reveal_block}" + ) + subtensor.wait_for_block(expected_reveal_block) + + # Fetch the latest drand pulse + latest_drand_round = subtensor.chain.last_drand_round() + logging.console.info( + f"Latest drand round after waiting for tempo: {latest_drand_round}" + ) + + # Fetch weights on the chain as they should be revealed now + subnet_weights = subtensor.subnets.weights(netuid=alice_subnet_netuid) + assert subnet_weights != [], "Weights are not available yet." + + logging.console.info(f"Revealed weights: {subnet_weights}") + + revealed_weights = subnet_weights[0][1] + # Assert correct weights were revealed + assert weight_uids[0] == revealed_weights[0][0] + assert weight_vals[0] == revealed_weights[0][1] + + logging.console.success( + f"Successfully revealed weights: uids {weight_uids}, weights {weight_vals}" + ) + + # Now that the commit has been revealed, there shouldn't be any pending commits + assert ( + subtensor.commitments.get_timelocked_weight_commits(netuid=alice_subnet_netuid) + == [] + ) + + # Ensure the drand_round is always in the positive w.r.t expected when revealed + assert latest_drand_round - expected_reveal_round >= -3, ( + f"latest_drand_round ({latest_drand_round}) is less than expected_reveal_round ({expected_reveal_round})" + ) + + logging.console.success("✅ Passed `test_commit_and_reveal_weights_cr4`") + + +@pytest.mark.asyncio +async def test_commit_and_reveal_weights_cr4_async( + async_subtensor, alice_wallet, local_chain +): + """ + Tests the commit/reveal weights mechanism (CR3) + + Steps: + 1. Register a subnet through Alice + 2. Register Alice's neuron and add stake + 3. Enable a commit-reveal mechanism on subnet + 4. Lower weights rate limit + 5. Change the tempo for subnet 1 + 5. Commit weights and ensure they are committed. + 6. Wait interval & reveal weights and verify + Raises: + AssertionError: If any of the checks or verifications fail + """ + logging.console.info("Testing `test_commit_and_reveal_weights_cr4`") + + async with async_subtensor: + # 12 for non-fast-block, 0.25 for fast block + BLOCK_TIME, TEMPO_TO_SET = ( + (0.25, 100) if await async_subtensor.chain.is_fast_blocks() else (12.0, 20) + ) + + logging.console.info(f"Using block time: {BLOCK_TIME} seconds.") + + alice_subnet_netuid = await async_subtensor.subnets.get_total_subnets() # 2 + + # Register root as Alice + assert await async_subtensor.extrinsics.register_subnet(alice_wallet), ( + "Unable to register the subnet" + ) + + # Verify subnet 2 created successfully + assert await async_subtensor.subnets.subnet_exists(alice_subnet_netuid), ( + f"SN #{alice_subnet_netuid} wasn't created successfully" + ) + + logging.console.success(f"SN #{alice_subnet_netuid} is registered.") + + # Enable commit_reveal on the subnet + assert await async_sudo_set_hyperparameter_bool( + substrate=async_subtensor.substrate, + wallet=alice_wallet, + call_function="sudo_set_commit_reveal_weights_enabled", + value=True, + netuid=alice_subnet_netuid, + ), f"Unable to enable commit reveal on the SN #{alice_subnet_netuid}" + + # Verify commit_reveal was enabled + assert await async_subtensor.subnets.commit_reveal_enabled( + alice_subnet_netuid + ), "Failed to enable commit/reveal" + logging.console.success("Commit reveal enabled") + + cr_version = await async_subtensor.substrate.query( + module="SubtensorModule", storage_function="CommitRevealWeightsVersion" + ) + assert cr_version == 4, f"Commit reveal version is not 3, got {cr_version}" + + # Change the weights rate limit on the subnet + status, error = sudo_set_admin_utils( + substrate=local_chain, + wallet=alice_wallet, + call_function="sudo_set_weights_set_rate_limit", + call_params={"netuid": alice_subnet_netuid, "weights_set_rate_limit": "0"}, + ) + + assert status is True + assert error is None + + # Verify weights rate limit was changed + assert ( + await async_subtensor.subnets.get_subnet_hyperparameters( + netuid=alice_subnet_netuid + ) + ).weights_rate_limit == 0, "Failed to set weights_rate_limit" + assert ( + await async_subtensor.subnets.weights_rate_limit(netuid=alice_subnet_netuid) + == 0 + ) + logging.console.success("sudo_set_weights_set_rate_limit executed: set to 0") + + # Change the tempo of the subnet + assert ( + sudo_set_admin_utils( + local_chain, + alice_wallet, + call_function="sudo_set_tempo", + call_params={"netuid": alice_subnet_netuid, "tempo": TEMPO_TO_SET}, + )[0] + is True + ) + + tempo = ( + await async_subtensor.subnets.get_subnet_hyperparameters( + netuid=alice_subnet_netuid + ) + ).tempo + assert tempo == TEMPO_TO_SET, "SN tempos has not been changed." + logging.console.success( + f"SN #{alice_subnet_netuid} tempo set to {TEMPO_TO_SET}" + ) + + # Commit-reveal values - setting weights to self + uids = np.array([0], dtype=np.int64) + weights = np.array([0.1], dtype=np.float32) + + weight_uids, weight_vals = convert_weights_and_uids_for_emit( + uids=uids, weights=weights + ) + logging.console.info( + f"Committing weights: uids {weight_uids}, weights {weight_vals}" + ) + + # Fetch current block and calculate next tempo for the subnet + current_block = await async_subtensor.chain.get_current_block() + upcoming_tempo = next_tempo(current_block, tempo) + logging.console.info( + f"Checking if window is too low with Current block: {current_block}, next tempo: {upcoming_tempo}" + ) + + # Lower than this might mean weights will get revealed before we can check them + if upcoming_tempo - current_block < 6: + await async_wait_interval( + tempo, + async_subtensor, + netuid=alice_subnet_netuid, + reporting_interval=1, + ) + current_block = await async_subtensor.chain.get_current_block() + latest_drand_round = await async_subtensor.chain.last_drand_round() + upcoming_tempo = next_tempo(current_block, tempo) + logging.console.info( + f"Post first wait_interval (to ensure window isn't too low): {current_block}, next tempo: {upcoming_tempo}, drand: {latest_drand_round}" + ) + + # Commit weights + success, message = await async_subtensor.extrinsics.set_weights( + wallet=alice_wallet, + netuid=alice_subnet_netuid, + uids=weight_uids, + weights=weight_vals, + wait_for_inclusion=True, + wait_for_finalization=True, + block_time=BLOCK_TIME, + period=16, + ) + + # Assert committing was a success + assert success is True, message + assert bool(re.match(r"reveal_round:\d+", message)) + + # Parse expected reveal_round + expected_reveal_round = int(message.split(":")[1]) + logging.console.success( + f"Successfully set weights: uids {weight_uids}, weights {weight_vals}, reveal_round: {expected_reveal_round}" + ) + + # Fetch current commits pending on the chain + commits_on_chain = ( + await async_subtensor.commitments.get_timelocked_weight_commits( + netuid=alice_subnet_netuid + ) + ) + address, commit_block, commit, reveal_round = commits_on_chain[0] + + # Assert correct values are committed on the chain + assert expected_reveal_round == reveal_round + assert address == alice_wallet.hotkey.ss58_address + + # bc of the drand delay, the commit block can be either the previous block or the current block + # assert expected_commit_block in [commit_block - 1, commit_block, commit_block + 1] + + # Ensure no weights are available as of now + assert await async_subtensor.subnets.weights(netuid=alice_subnet_netuid) == [] + logging.console.success("No weights are available before next epoch.") + + # 5 is safety drand offset + expected_reveal_block = ( + await async_subtensor.subnets.get_next_epoch_start_block( + alice_subnet_netuid + ) + + 5 + ) + + logging.console.info( + f"Waiting for the next epoch to ensure weights are revealed: block {expected_reveal_block}" + ) + await async_subtensor.wait_for_block(expected_reveal_block) + + # Fetch the latest drand pulse + latest_drand_round = await async_subtensor.chain.last_drand_round() + logging.console.info( + f"Latest drand round after waiting for tempo: {latest_drand_round}" + ) + + # Fetch weights on the chain as they should be revealed now + subnet_weights = await async_subtensor.subnets.weights( + netuid=alice_subnet_netuid + ) + assert subnet_weights != [], "Weights are not available yet." + + logging.console.info(f"Revealed weights: {subnet_weights}") + + revealed_weights = subnet_weights[0][1] + # Assert correct weights were revealed + assert weight_uids[0] == revealed_weights[0][0] + assert weight_vals[0] == revealed_weights[0][1] + + logging.console.success( + f"Successfully revealed weights: uids {weight_uids}, weights {weight_vals}" + ) + + # Now that the commit has been revealed, there shouldn't be any pending commits + assert ( + await async_subtensor.commitments.get_timelocked_weight_commits( + netuid=alice_subnet_netuid + ) + == [] + ) + + # Ensure the drand_round is always in the positive w.r.t expected when revealed + assert latest_drand_round - expected_reveal_round >= -3, ( + f"latest_drand_round ({latest_drand_round}) is less than expected_reveal_round ({expected_reveal_round})" + ) + + logging.console.success("✅ Passed `test_commit_and_reveal_weights_cr4`") diff --git a/tests/e2e_tests/test_commit_weights.py b/tests/e2e_tests/test_commit_weights.py new file mode 100644 index 0000000000..2eb288cd14 --- /dev/null +++ b/tests/e2e_tests/test_commit_weights.py @@ -0,0 +1,655 @@ +import numpy as np +import pytest +import retry +import time + +from bittensor.utils.btlogging import logging +from bittensor.utils.weight_utils import convert_weights_and_uids_for_emit +from tests.e2e_tests.utils.chain_interactions import ( + async_sudo_set_admin_utils, + async_sudo_set_hyperparameter_bool, + async_wait_epoch, + sudo_set_admin_utils, + sudo_set_hyperparameter_bool, + execute_and_wait_for_next_nonce, + wait_epoch, +) + + +@pytest.mark.asyncio +async def test_commit_and_reveal_weights_legacy(subtensor, alice_wallet): + """ + Tests the commit/reveal weights mechanism with subprocess disabled (CR1.0) + + Steps: + 1. Register a subnet through Alice + 2. Enable the commit-reveal mechanism on subnet + 3. Lower the commit_reveal interval and rate limit + 4. Commit weights and verify + 5. Wait interval & reveal weights and verify + Raises: + AssertionError: If any of the checks or verifications fail + """ + logging.console.info("Testing test_commit_and_reveal_weights") + + netuid = subtensor.subnets.get_total_subnets() # 2 + set_tempo = 100 if subtensor.chain.is_fast_blocks() else 10 + + # Register root as Alice + assert subtensor.subnets.register_subnet(alice_wallet), ( + "Unable to register the subnet" + ) + + # Verify subnet 2 created successfully + assert subtensor.subnets.subnet_exists(netuid), "Subnet wasn't created successfully" + + # Enable commit_reveal on the subnet + assert sudo_set_hyperparameter_bool( + substrate=subtensor.substrate, + wallet=alice_wallet, + call_function="sudo_set_commit_reveal_weights_enabled", + value=True, + netuid=netuid, + ), "Unable to enable commit reveal on the subnet" + + assert subtensor.subnets.commit_reveal_enabled(netuid), ( + "Failed to enable commit/reveal" + ) + + assert ( + subtensor.subnets.get_subnet_hyperparameters(netuid=netuid).commit_reveal_period + == 1 + ), "Failed to set commit/reveal periods" + + assert subtensor.subnets.weights_rate_limit(netuid=netuid) > 0, ( + "Weights rate limit is below 0" + ) + + # Lower the rate limit + status, error = sudo_set_admin_utils( + substrate=subtensor.substrate, + wallet=alice_wallet, + call_function="sudo_set_weights_set_rate_limit", + call_params={"netuid": netuid, "weights_set_rate_limit": "0"}, + ) + + assert error is None + assert status is True + + assert ( + subtensor.subnets.get_subnet_hyperparameters(netuid=netuid).weights_rate_limit + == 0 + ), "Failed to set weights_rate_limit" + assert subtensor.subnets.weights_rate_limit(netuid=netuid) == 0 + + # Increase subnet tempo so we have enough time to commit and reveal weights + sudo_set_admin_utils( + substrate=subtensor.substrate, + wallet=alice_wallet, + call_function="sudo_set_tempo", + call_params={ + "netuid": netuid, + "tempo": set_tempo, + }, + ) + + # Commit-reveal values + uids = np.array([0], dtype=np.int64) + weights = np.array([0.1], dtype=np.float32) + salt = [18, 179, 107, 0, 165, 211, 141, 197] + weight_uids, weight_vals = convert_weights_and_uids_for_emit( + uids=uids, weights=weights + ) + + # Commit weights + success, message = subtensor.extrinsics.commit_weights( + alice_wallet, + netuid, + salt=salt, + uids=weight_uids, + weights=weight_vals, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + assert success is True + + weight_commits = subtensor.queries.query_module( + module="SubtensorModule", + name="WeightCommits", + params=[netuid, alice_wallet.hotkey.ss58_address], + ) + # Assert that the committed weights are set correctly + assert weight_commits is not None, "Weight commit not found in storage" + commit_hash, commit_block, reveal_block, expire_block = weight_commits[0] + assert commit_block > 0, f"Invalid block number: {commit_block}" + + # Query the WeightCommitRevealInterval storage map + assert subtensor.subnets.get_subnet_reveal_period_epochs(netuid) > 0, ( + "Invalid RevealPeriodEpochs" + ) + + # Wait until the reveal block range + await wait_epoch(subtensor, netuid) + + # Reveal weights + success, message = subtensor.extrinsics.reveal_weights( + alice_wallet, + netuid, + uids=weight_uids, + weights=weight_vals, + salt=salt, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + assert success is True + + # Query the Weights storage map + revealed_weights = subtensor.queries.query_module( + module="SubtensorModule", + name="Weights", + params=[netuid, 0], # netuid and uid + ) + + # Assert that the revealed weights are set correctly + assert revealed_weights is not None, "Weight reveal not found in storage" + + assert weight_vals[0] == revealed_weights[0][1], ( + f"Incorrect revealed weights. Expected: {weights[0]}, Actual: {revealed_weights[0][1]}" + ) + logging.console.success("✅ Passed test_commit_and_reveal_weights") + + +@pytest.mark.asyncio +async def test_commit_and_reveal_weights_legacy_async(async_subtensor, alice_wallet): + """ + Tests the commit/reveal weights mechanism with subprocess disabled (CR1.0) with AsyncSubtensor. + + Steps: + 1. Register a subnet through Alice + 2. Enable the commit-reveal mechanism on subnet + 3. Lower the commit_reveal interval and rate limit + 4. Commit weights and verify + 5. Wait interval & reveal weights and verify + Raises: + AssertionError: If any of the checks or verifications fail + """ + logging.console.info("Testing test_commit_and_reveal_weights_async") + + netuid = await async_subtensor.subnets.get_total_subnets() # 2 + set_tempo = 100 if await async_subtensor.chain.is_fast_blocks() else 10 + + # Register root as Alice + assert await async_subtensor.subnets.register_subnet(alice_wallet), ( + "Unable to register the subnet" + ) + + # Verify subnet 2 created successfully + assert await async_subtensor.subnets.subnet_exists(netuid), ( + "Subnet wasn't created successfully" + ) + + # Enable commit_reveal on the subnet + assert await async_sudo_set_hyperparameter_bool( + substrate=async_subtensor.substrate, + wallet=alice_wallet, + call_function="sudo_set_commit_reveal_weights_enabled", + value=True, + netuid=netuid, + ), "Unable to enable commit reveal on the subnet" + + assert await async_subtensor.subnets.commit_reveal_enabled(netuid), ( + "Failed to enable commit/reveal" + ) + + assert ( + await async_subtensor.subnets.get_subnet_hyperparameters(netuid=netuid) + ).commit_reveal_period == 1, "Failed to set commit/reveal periods" + + assert await async_subtensor.subnets.weights_rate_limit(netuid=netuid) > 0, ( + "Weights rate limit is below 0" + ) + + # Lower the rate limit + status, error = await async_sudo_set_admin_utils( + substrate=async_subtensor.substrate, + wallet=alice_wallet, + call_function="sudo_set_weights_set_rate_limit", + call_params={"netuid": netuid, "weights_set_rate_limit": "0"}, + ) + + assert error is None + assert status is True + + assert ( + await async_subtensor.subnets.get_subnet_hyperparameters(netuid=netuid) + ).weights_rate_limit == 0, "Failed to set weights_rate_limit" + assert await async_subtensor.subnets.weights_rate_limit(netuid=netuid) == 0 + + # Increase subnet tempo so we have enough time to commit and reveal weights + await async_sudo_set_admin_utils( + substrate=async_subtensor.substrate, + wallet=alice_wallet, + call_function="sudo_set_tempo", + call_params={ + "netuid": netuid, + "tempo": set_tempo, + }, + ) + + # Commit-reveal values + uids = np.array([0], dtype=np.int64) + weights = np.array([0.1], dtype=np.float32) + salt = [18, 179, 107, 0, 165, 211, 141, 197] + weight_uids, weight_vals = convert_weights_and_uids_for_emit( + uids=uids, weights=weights + ) + + # Commit weights + success, message = await async_subtensor.extrinsics.commit_weights( + wallet=alice_wallet, + netuid=netuid, + salt=salt, + uids=weight_uids, + weights=weight_vals, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + assert success is True + + weight_commits = await async_subtensor.queries.query_module( + module="SubtensorModule", + name="WeightCommits", + params=[netuid, alice_wallet.hotkey.ss58_address], + ) + # Assert that the committed weights are set correctly + assert weight_commits is not None, "Weight commit not found in storage" + commit_hash, commit_block, reveal_block, expire_block = weight_commits[0] + assert commit_block > 0, f"Invalid block number: {commit_block}" + + # Query the WeightCommitRevealInterval storage map + assert await async_subtensor.subnets.get_subnet_reveal_period_epochs(netuid) > 0, ( + "Invalid RevealPeriodEpochs" + ) + + # Wait until the reveal block range + await async_wait_epoch(async_subtensor, netuid) + + # Reveal weights + success, message = await async_subtensor.extrinsics.reveal_weights( + alice_wallet, + netuid, + uids=weight_uids, + weights=weight_vals, + salt=salt, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + assert success is True + + # Query the Weights storage map + revealed_weights = await async_subtensor.queries.query_module( + module="SubtensorModule", + name="Weights", + params=[netuid, 0], # netuid and uid + ) + + # Assert that the revealed weights are set correctly + assert revealed_weights is not None, "Weight reveal not found in storage" + + assert weight_vals[0] == revealed_weights[0][1], ( + f"Incorrect revealed weights. Expected: {weights[0]}, Actual: {revealed_weights[0][1]}" + ) + logging.console.success("✅ Passed test_commit_and_reveal_weights_async") + + +# Create different committed data to avoid coming into the pool's blacklist with the error +# Failed to commit weights: Subtensor returned `Custom type(1012)` error. This means: `Transaction is temporarily +# banned`.Failed to commit weights: Subtensor returned `Custom type(1012)` error. This means: `Transaction is +# temporarily banned`.` +def get_weights_and_salt(counter: int): + # Commit-reveal values + salt_ = [18, 179, 107, counter, 165, 211, 141, 197] + uids_ = np.array([0], dtype=np.int64) + weights_ = np.array([counter / 10], dtype=np.float32) + weight_uids_, weight_vals_ = convert_weights_and_uids_for_emit( + uids=uids_, weights=weights_ + ) + return salt_, weight_uids_, weight_vals_ + + +@pytest.mark.asyncio +async def test_commit_weights_uses_next_nonce(subtensor, alice_wallet): + """ + Tests that committing weights doesn't re-use nonce in the transaction pool. + + Steps: + 1. Register a subnet through Alice + 2. Register Alice's neuron and add stake + 3. Enable the commit-reveal mechanism on subnet + 4. Lower the commit_reveal interval and rate limit + 5. Commit weights three times + 6. Assert that all commits succeeded + Raises: + AssertionError: If any of the checks or verifications fail + """ + logging.console.info("Testing test_commit_and_reveal_weights") + + subnet_tempo = 50 if subtensor.chain.is_fast_blocks() else 10 + netuid = subtensor.subnets.get_total_subnets() # 2 + + # Wait for 2 tempos to pass as CR3 only reveals weights after 2 tempos + subtensor.wait_for_block(subtensor.block + (subnet_tempo * 2) + 1) + + # Register root as Alice + assert subtensor.subnets.register_subnet(alice_wallet), ( + "Unable to register the subnet" + ) + + # Verify subnet 1 created successfully + assert subtensor.subnets.subnet_exists(netuid), "Subnet wasn't created successfully" + + # weights sensitive to epoch changes + assert sudo_set_admin_utils( + substrate=subtensor.substrate, + wallet=alice_wallet, + call_function="sudo_set_tempo", + call_params={ + "netuid": netuid, + "tempo": subnet_tempo, + }, + ) + + # Enable commit_reveal on the subnet + assert sudo_set_hyperparameter_bool( + substrate=subtensor.substrate, + wallet=alice_wallet, + call_function="sudo_set_commit_reveal_weights_enabled", + value=True, + netuid=netuid, + ), "Unable to enable commit reveal on the subnet" + + assert subtensor.commitments.commit_reveal_enabled(netuid), ( + "Failed to enable commit/reveal" + ) + + assert ( + subtensor.subnets.get_subnet_hyperparameters(netuid=netuid).commit_reveal_period + == 1 + ), "Failed to set commit/reveal periods" + + assert subtensor.subnets.weights_rate_limit(netuid=netuid) > 0, ( + "Weights rate limit is below 0" + ) + + # Lower the rate limit + status, error = sudo_set_admin_utils( + substrate=subtensor.substrate, + wallet=alice_wallet, + call_function="sudo_set_weights_set_rate_limit", + call_params={"netuid": netuid, "weights_set_rate_limit": "0"}, + ) + + assert error is None and status is True, f"Failed to set rate limit: {error}" + + assert ( + subtensor.subnets.get_subnet_hyperparameters(netuid=netuid).weights_rate_limit + == 0 + ), "Failed to set weights_rate_limit" + assert subtensor.subnets.weights_rate_limit(netuid=netuid) == 0 + + # wait while weights_rate_limit changes applied. + subtensor.wait_for_block(subnet_tempo + 1) + + logging.console.info( + f"[orange]Nonce before first commit_weights: " + f"{subtensor.substrate.get_account_next_index(alice_wallet.hotkey.ss58_address)}[/orange]" + ) + + # 3 time doing call if nonce wasn't updated, then raise the error + @retry.retry(exceptions=Exception, tries=3, delay=1) + @execute_and_wait_for_next_nonce(subtensor=subtensor, wallet=alice_wallet) + def send_commit(salt_, weight_uids_, weight_vals_): + success, message = subtensor.extrinsics.commit_weights( + wallet=alice_wallet, + netuid=netuid, + salt=salt_, + uids=weight_uids_, + weights=weight_vals_, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert success is True, message + + # Send some number of commit weights + AMOUNT_OF_COMMIT_WEIGHTS = 3 + for call in range(AMOUNT_OF_COMMIT_WEIGHTS): + weight_uids, weight_vals, salt = get_weights_and_salt(call) + + send_commit(salt, weight_uids, weight_vals) + + # let's wait for 3 (12 fast blocks) seconds between transactions, next block for non-fast-blocks + waiting_block = ( + (subtensor.block + 12) if subtensor.chain.is_fast_blocks() else None + ) + subtensor.wait_for_block(waiting_block) + + logging.console.info( + f"[orange]Nonce after third commit_weights: " + f"{subtensor.substrate.get_account_next_index(alice_wallet.hotkey.ss58_address)}[/orange]" + ) + + # Wait a few blocks + waiting_block = ( + (subtensor.block + subtensor.subnets.tempo(netuid) * 2) + if subtensor.chain.is_fast_blocks() + else None + ) + subtensor.wait_for_block(waiting_block) + + # Query the WeightCommits storage map for all three salts + weight_commits = subtensor.queries.query_module( + module="SubtensorModule", + name="WeightCommits", + params=[netuid, alice_wallet.hotkey.ss58_address], + ) + # Assert that the committed weights are set correctly + assert weight_commits.value is not None, "Weight commit not found in storage" + commit_hash, commit_block, reveal_block, expire_block = weight_commits.value[0] + assert commit_block > 0, f"Invalid block number: {commit_block}" + + # Check for three commits in the WeightCommits storage map + assert len(weight_commits.value) == AMOUNT_OF_COMMIT_WEIGHTS, ( + "Expected exact list of weight commits" + ) + logging.console.success("✅ Passed `test_commit_and_reveal_weights` test.") + + +@pytest.mark.asyncio +async def test_commit_weights_uses_next_nonce_async(async_subtensor, alice_wallet): + """ + Tests that committing weights doesn't re-use nonce in the transaction pool with AsyncSubtensor. + + Steps: + 1. Register a subnet through Alice + 2. Register Alice's neuron and add stake + 3. Enable the commit-reveal mechanism on subnet + 4. Lower the commit_reveal interval and rate limit + 5. Commit weights three times + 6. Assert that all commits succeeded + Raises: + AssertionError: If any of the checks or verifications fail + """ + logging.console.info("Testing test_commit_and_reveal_weights") + + subnet_tempo = 50 if await async_subtensor.chain.is_fast_blocks() else 10 + netuid = await async_subtensor.subnets.get_total_subnets() # 2 + + # Wait for 2 tempos to pass as CR3 only reveals weights after 2 tempos + await async_subtensor.wait_for_block( + await async_subtensor.block + (subnet_tempo * 2) + 1 + ) + + # Register root as Alice + assert await async_subtensor.subnets.register_subnet(alice_wallet), ( + "Unable to register the subnet" + ) + + # Verify subnet 1 created successfully + assert await async_subtensor.subnets.subnet_exists(netuid), ( + "Subnet wasn't created successfully" + ) + + # weights sensitive to epoch changes + assert await async_sudo_set_admin_utils( + substrate=async_subtensor.substrate, + wallet=alice_wallet, + call_function="sudo_set_tempo", + call_params={ + "netuid": netuid, + "tempo": subnet_tempo, + }, + ) + + # Enable commit_reveal on the subnet + assert await async_sudo_set_hyperparameter_bool( + substrate=async_subtensor.substrate, + wallet=alice_wallet, + call_function="sudo_set_commit_reveal_weights_enabled", + value=True, + netuid=netuid, + ), "Unable to enable commit reveal on the subnet" + + assert await async_subtensor.commitments.commit_reveal_enabled(netuid), ( + "Failed to enable commit/reveal" + ) + + assert ( + await async_subtensor.subnets.get_subnet_hyperparameters(netuid=netuid) + ).commit_reveal_period == 1, "Failed to set commit/reveal periods" + + assert await async_subtensor.subnets.weights_rate_limit(netuid=netuid) > 0, ( + "Weights rate limit is below 0" + ) + + # Lower the rate limit + status, error = await async_sudo_set_admin_utils( + substrate=async_subtensor.substrate, + wallet=alice_wallet, + call_function="sudo_set_weights_set_rate_limit", + call_params={"netuid": netuid, "weights_set_rate_limit": "0"}, + ) + + assert error is None and status is True, f"Failed to set rate limit: {error}" + + assert ( + await async_subtensor.subnets.get_subnet_hyperparameters(netuid=netuid) + ).weights_rate_limit == 0, "Failed to set weights_rate_limit" + assert await async_subtensor.subnets.weights_rate_limit(netuid=netuid) == 0 + + # wait while weights_rate_limit changes applied. + await async_subtensor.wait_for_block(subnet_tempo + 1) + + logging.console.info( + f"[orange]Nonce before first commit_weights: " + f"{await async_subtensor.substrate.get_account_nonce(alice_wallet.hotkey.ss58_address)}[/orange]" + ) + + # 3 time doing call if nonce wasn't updated, then raise the error + + async def send_commit(salt_, weight_uids_, weight_vals_): + """ + To avoid adding asynchronous retrieval to dependencies, we implement a retrieval behavior with asynchronous + behavior. + """ + + async def send_commit_(): + success_, message_ = await async_subtensor.extrinsics.commit_weights( + wallet=alice_wallet, + netuid=netuid, + salt=salt_, + uids=weight_uids_, + weights=weight_vals_, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + return success_, message_ + + max_retries = 3 + timeout = 60.0 + sleep = 0.25 if async_subtensor.chain.is_fast_blocks() else 12.0 + + for attempt in range(1, max_retries + 1): + try: + start_nonce = await async_subtensor.substrate.get_account_nonce( + alice_wallet.hotkey.ss58_address + ) + + result = await send_commit_() + + start = time.time() + while (time.time() - start) < timeout: + current_nonce = await async_subtensor.substrate.get_account_nonce( + alice_wallet.hotkey.ss58_address + ) + + if current_nonce != start_nonce: + logging.console.info( + f"✅ Nonce changed from {start_nonce} to {current_nonce}" + ) + return result + logging.console.info( + f"⏳ Waiting for nonce increment. Current: {current_nonce}" + ) + time.sleep(sleep) + except Exception as e: + raise e + raise Exception(f"Failed to commit weights after {max_retries} attempts.") + + # Send some number of commit weights + AMOUNT_OF_COMMIT_WEIGHTS = 3 + for call in range(AMOUNT_OF_COMMIT_WEIGHTS): + weight_uids, weight_vals, salt = get_weights_and_salt(call) + + await send_commit(salt, weight_uids, weight_vals) + + # let's wait for 3 (12 fast blocks) seconds between transactions, next block for non-fast-blocks + waiting_block = ( + (await async_subtensor.block + 12) + if await async_subtensor.chain.is_fast_blocks() + else None + ) + await async_subtensor.wait_for_block(waiting_block) + + logging.console.info( + f"[orange]Nonce after third commit_weights: " + f"{await async_subtensor.substrate.get_account_next_index(alice_wallet.hotkey.ss58_address)}[/orange]" + ) + + # Wait a few blocks + waiting_block = ( + (await async_subtensor.block + await async_subtensor.subnets.tempo(netuid) * 2) + if await async_subtensor.chain.is_fast_blocks() + else None + ) + await async_subtensor.wait_for_block(waiting_block) + + # Query the WeightCommits storage map for all three salts + weight_commits = await async_subtensor.queries.query_module( + module="SubtensorModule", + name="WeightCommits", + params=[netuid, alice_wallet.hotkey.ss58_address], + ) + # Assert that the committed weights are set correctly + assert weight_commits.value is not None, "Weight commit not found in storage" + commit_hash, commit_block, reveal_block, expire_block = weight_commits.value[0] + assert commit_block > 0, f"Invalid block number: {commit_block}" + + # Check for three commits in the WeightCommits storage map + assert len(weight_commits.value) == AMOUNT_OF_COMMIT_WEIGHTS, ( + "Expected exact list of weight commits" + ) + logging.console.success("✅ Passed `test_commit_and_reveal_weights_async` test.") diff --git a/tests/e2e_tests/test_commitment.py b/tests/e2e_tests/test_commitment.py new file mode 100644 index 0000000000..d31bf739ef --- /dev/null +++ b/tests/e2e_tests/test_commitment.py @@ -0,0 +1,165 @@ +import pytest +from async_substrate_interface.errors import SubstrateRequestException + +from bittensor import logging +from tests.e2e_tests.utils.chain_interactions import ( + async_sudo_set_admin_utils, + sudo_set_admin_utils, +) +from tests.e2e_tests.utils.e2e_test_utils import ( + wait_to_start_call, + async_wait_to_start_call, +) + +logging.set_trace() + + +def test_commitment(subtensor, alice_wallet, dave_wallet): + dave_subnet_netuid = 2 + assert subtensor.subnets.register_subnet(dave_wallet) + assert subtensor.subnets.subnet_exists(dave_subnet_netuid), ( + "Subnet wasn't created successfully" + ) + + assert wait_to_start_call(subtensor, dave_wallet, dave_subnet_netuid) + + with pytest.raises(SubstrateRequestException, match="AccountNotAllowedCommit"): + subtensor.commitments.set_commitment( + alice_wallet, + netuid=dave_subnet_netuid, + data="Hello World!", + ) + + assert subtensor.subnets.burned_register( + wallet=alice_wallet, + netuid=dave_subnet_netuid, + ) + + uid = subtensor.subnets.get_uid_for_hotkey_on_subnet( + hotkey_ss58=alice_wallet.hotkey.ss58_address, + netuid=dave_subnet_netuid, + ) + + assert uid is not None + + assert "" == subtensor.commitments.get_commitment( + netuid=dave_subnet_netuid, + uid=uid, + ) + + assert subtensor.commitments.set_commitment( + wallet=alice_wallet, + netuid=dave_subnet_netuid, + data="Hello World!", + ) + + status, error = sudo_set_admin_utils( + substrate=subtensor.substrate, + wallet=alice_wallet, + call_module="Commitments", + call_function="set_max_space", + call_params={ + "netuid": dave_subnet_netuid, + "new_limit": len("Hello World!"), + }, + ) + + assert status is True, error + + with pytest.raises( + SubstrateRequestException, + match="SpaceLimitExceeded", + ): + subtensor.commitments.set_commitment( + wallet=alice_wallet, + netuid=dave_subnet_netuid, + data="Hello World!1", + ) + + assert "Hello World!" == subtensor.commitments.get_commitment( + netuid=dave_subnet_netuid, + uid=uid, + ) + + assert ( + subtensor.commitments.get_all_commitments(netuid=dave_subnet_netuid)[ + alice_wallet.hotkey.ss58_address + ] + == "Hello World!" + ) + + +@pytest.mark.asyncio +async def test_commitment_async(async_subtensor, alice_wallet, dave_wallet): + dave_subnet_netuid = 2 + assert await async_subtensor.subnets.register_subnet(dave_wallet) + assert await async_subtensor.subnets.subnet_exists(dave_subnet_netuid), ( + "Subnet wasn't created successfully" + ) + + assert await async_wait_to_start_call( + async_subtensor, dave_wallet, dave_subnet_netuid + ) + + async with async_subtensor as sub: + with pytest.raises(SubstrateRequestException, match="AccountNotAllowedCommit"): + await sub.commitments.set_commitment( + alice_wallet, + netuid=dave_subnet_netuid, + data="Hello World!", + ) + + assert await sub.subnets.burned_register( + wallet=alice_wallet, + netuid=dave_subnet_netuid, + ) + + uid = await sub.subnets.get_uid_for_hotkey_on_subnet( + alice_wallet.hotkey.ss58_address, + netuid=dave_subnet_netuid, + ) + + assert uid is not None + + assert "" == await sub.commitments.get_commitment( + netuid=dave_subnet_netuid, + uid=uid, + ) + + assert await sub.commitments.set_commitment( + alice_wallet, + netuid=dave_subnet_netuid, + data="Hello World!", + ) + + status, error = await async_sudo_set_admin_utils( + substrate=async_subtensor.substrate, + wallet=alice_wallet, + call_module="Commitments", + call_function="set_max_space", + call_params={ + "netuid": dave_subnet_netuid, + "new_limit": len("Hello World!"), + }, + ) + + assert status is True, error + + with pytest.raises( + SubstrateRequestException, + match="SpaceLimitExceeded", + ): + await sub.commitments.set_commitment( + alice_wallet, + netuid=dave_subnet_netuid, + data="Hello World!1", + ) + + assert "Hello World!" == await sub.commitments.get_commitment( + netuid=dave_subnet_netuid, + uid=uid, + ) + + assert (await sub.commitments.get_all_commitments(netuid=dave_subnet_netuid))[ + alice_wallet.hotkey.ss58_address + ] == "Hello World!" diff --git a/tests/e2e_tests/test_cross_subtensor_compatibility.py b/tests/e2e_tests/test_cross_subtensor_compatibility.py new file mode 100644 index 0000000000..44164b589e --- /dev/null +++ b/tests/e2e_tests/test_cross_subtensor_compatibility.py @@ -0,0 +1,19 @@ +from datetime import datetime + +import pytest + + +@pytest.mark.asyncio +async def test_get_timestamp(subtensor, async_subtensor): + with subtensor: + block_number = subtensor.chain.get_current_block() + assert isinstance( + subtensor.chain.get_timestamp(), datetime + ) # verify it works with no block number specified + sync_result = subtensor.chain.get_timestamp( + block=block_number + ) # verify it works with block number specified + async with async_subtensor: + assert isinstance(await async_subtensor.chain.get_timestamp(), datetime) + async_result = await async_subtensor.chain.get_timestamp(block=block_number) + assert sync_result == async_result diff --git a/tests/e2e_tests/test_delegate.py b/tests/e2e_tests/test_delegate.py new file mode 100644 index 0000000000..04417f6615 --- /dev/null +++ b/tests/e2e_tests/test_delegate.py @@ -0,0 +1,1017 @@ +import pytest + +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, + HotKeyAccountNotExists, + NonAssociatedColdKey, +) +from bittensor.utils.balance import Balance +from bittensor.utils.btlogging import logging +from tests.e2e_tests.utils.chain_interactions import ( + async_propose, + async_set_identity, + async_sudo_set_admin_utils, + async_vote, + get_dynamic_balance, + propose, + set_identity, + sudo_set_admin_utils, + vote, +) +from tests.e2e_tests.utils.e2e_test_utils import ( + async_wait_to_start_call, + wait_to_start_call, +) +from tests.helpers.helpers import CloseInValue + +DEFAULT_DELEGATE_TAKE = 0.179995422293431 + + +def test_identity(subtensor, alice_wallet, bob_wallet): + """ + Tests: + - Check Delegate's default identity + - Update Delegate's identity + """ + logging.console.info("Testing [green]test_identity[/green].") + + identity = subtensor.neurons.query_identity(alice_wallet.coldkeypub.ss58_address) + assert identity is None + + identities = subtensor.delegates.get_delegate_identities() + assert alice_wallet.coldkey.ss58_address not in identities + + subtensor.extrinsics.root_register( + alice_wallet, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + identities = subtensor.delegates.get_delegate_identities() + assert alice_wallet.coldkey.ss58_address not in identities + + success, error = set_identity( + subtensor=subtensor, + wallet=alice_wallet, + name="Alice", + url="https://www.example.com", + github_repo="https://github.com/opentensor/bittensor", + description="Local Chain", + ) + assert error == "" + assert success is True + + identity = subtensor.neurons.query_identity(alice_wallet.coldkeypub.ss58_address) + assert identity == ChainIdentity( + additional="", + description="Local Chain", + discord="", + github="https://github.com/opentensor/bittensor", + image="", + name="Alice", + url="https://www.example.com", + ) + + identities = subtensor.delegates.get_delegate_identities() + assert alice_wallet.coldkey.ss58_address in identities + + identity = identities[alice_wallet.coldkey.ss58_address] + assert identity == ChainIdentity( + additional="", + description="Local Chain", + discord="", + github="https://github.com/opentensor/bittensor", + image="", + name="Alice", + url="https://www.example.com", + ) + logging.console.success("Test [green]test_identity[/green] passed.") + + +@pytest.mark.asyncio +async def test_identity_async(async_subtensor, alice_wallet, bob_wallet): + """ + Async tests: + - Check Delegate's default identity + - Update Delegate's identity + """ + logging.console.info("Testing [green]test_identity_async[/green].") + + identity = await async_subtensor.neurons.query_identity( + alice_wallet.coldkeypub.ss58_address + ) + assert identity is None + + identities = await async_subtensor.delegates.get_delegate_identities() + assert alice_wallet.coldkey.ss58_address not in identities + + await async_subtensor.extrinsics.root_register( + alice_wallet, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + identities = await async_subtensor.delegates.get_delegate_identities() + assert alice_wallet.coldkey.ss58_address not in identities + + success, error = await async_set_identity( + subtensor=async_subtensor, + wallet=alice_wallet, + name="Alice", + url="https://www.example.com", + github_repo="https://github.com/opentensor/bittensor", + description="Local Chain", + ) + + assert error == "" + assert success is True + + identity = await async_subtensor.neurons.query_identity( + alice_wallet.coldkeypub.ss58_address + ) + + assert identity == ChainIdentity( + additional="", + description="Local Chain", + discord="", + github="https://github.com/opentensor/bittensor", + image="", + name="Alice", + url="https://www.example.com", + ) + + identities = await async_subtensor.delegates.get_delegate_identities() + assert alice_wallet.coldkey.ss58_address in identities + + identity = identities[alice_wallet.coldkey.ss58_address] + assert identity == ChainIdentity( + additional="", + description="Local Chain", + discord="", + github="https://github.com/opentensor/bittensor", + image="", + name="Alice", + url="https://www.example.com", + ) + logging.console.success("Test [green]test_identity_async[/green] passed.") + + +def test_change_take(subtensor, alice_wallet, bob_wallet): + """ + Tests: + - Get default Delegate's take once registered in root subnet + - Increase and decreased Delegate's take + - Try corner cases (increase/decrease beyond allowed min/max) + """ + + logging.console.info("Testing [green]test_change_take[/green].") + with pytest.raises(HotKeyAccountNotExists): + subtensor.delegates.set_delegate_take( + alice_wallet, + alice_wallet.hotkey.ss58_address, + 0.1, + raise_error=True, + ) + + subtensor.extrinsics.root_register( + alice_wallet, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + take = subtensor.delegates.get_delegate_take(alice_wallet.hotkey.ss58_address) + assert take == DEFAULT_DELEGATE_TAKE + + with pytest.raises(NonAssociatedColdKey): + subtensor.delegates.set_delegate_take( + bob_wallet, + alice_wallet.hotkey.ss58_address, + 0.1, + raise_error=True, + ) + + with pytest.raises(DelegateTakeTooHigh): + subtensor.delegates.set_delegate_take( + alice_wallet, + alice_wallet.hotkey.ss58_address, + 0.5, + raise_error=True, + ) + + subtensor.delegates.set_delegate_take( + alice_wallet, + alice_wallet.hotkey.ss58_address, + 0.1, + raise_error=True, + ) + + take = subtensor.delegates.get_delegate_take(alice_wallet.hotkey.ss58_address) + assert take == 0.09999237048905166 + + with pytest.raises(DelegateTxRateLimitExceeded): + subtensor.delegates.set_delegate_take( + alice_wallet, + alice_wallet.hotkey.ss58_address, + 0.15, + raise_error=True, + ) + + take = subtensor.delegates.get_delegate_take(alice_wallet.hotkey.ss58_address) + assert take == 0.09999237048905166 + + sudo_set_admin_utils( + substrate=subtensor.substrate, + wallet=alice_wallet, + call_function="sudo_set_tx_delegate_take_rate_limit", + call_params={ + "tx_rate_limit": 0, + }, + ) + + subtensor.delegates.set_delegate_take( + wallet=alice_wallet, + hotkey_ss58=alice_wallet.hotkey.ss58_address, + take=0.15, + raise_error=True, + ) + + take = subtensor.delegates.get_delegate_take(alice_wallet.hotkey.ss58_address) + assert take == 0.14999618524452582 + + logging.console.success("Test [green]test_change_take[/green] passed.") + + +@pytest.mark.asyncio +async def test_change_take_async(async_subtensor, alice_wallet, bob_wallet): + """ + Async tests: + - Get default Delegate's take once registered in root subnet + - Increase and decreased Delegate's take + - Try corner cases (increase/decrease beyond allowed min/max) + """ + + logging.console.info("Testing [green]test_change_take_async[/green].") + with pytest.raises(HotKeyAccountNotExists): + await async_subtensor.delegates.set_delegate_take( + alice_wallet, + alice_wallet.hotkey.ss58_address, + 0.1, + raise_error=True, + ) + + await async_subtensor.extrinsics.root_register( + alice_wallet, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + take = await async_subtensor.delegates.get_delegate_take( + alice_wallet.hotkey.ss58_address + ) + assert take == DEFAULT_DELEGATE_TAKE + + with pytest.raises(NonAssociatedColdKey): + await async_subtensor.delegates.set_delegate_take( + bob_wallet, + alice_wallet.hotkey.ss58_address, + 0.1, + raise_error=True, + ) + + with pytest.raises(DelegateTakeTooHigh): + await async_subtensor.delegates.set_delegate_take( + alice_wallet, + alice_wallet.hotkey.ss58_address, + 0.5, + raise_error=True, + ) + + await async_subtensor.delegates.set_delegate_take( + alice_wallet, + alice_wallet.hotkey.ss58_address, + 0.1, + raise_error=True, + ) + + take = await async_subtensor.delegates.get_delegate_take( + alice_wallet.hotkey.ss58_address + ) + assert take == 0.09999237048905166 + + with pytest.raises(DelegateTxRateLimitExceeded): + await async_subtensor.delegates.set_delegate_take( + alice_wallet, + alice_wallet.hotkey.ss58_address, + 0.15, + raise_error=True, + ) + + take = await async_subtensor.delegates.get_delegate_take( + alice_wallet.hotkey.ss58_address + ) + assert take == 0.09999237048905166 + + await async_sudo_set_admin_utils( + substrate=async_subtensor.substrate, + wallet=alice_wallet, + call_function="sudo_set_tx_delegate_take_rate_limit", + call_params={ + "tx_rate_limit": 0, + }, + ) + + await async_subtensor.delegates.set_delegate_take( + wallet=alice_wallet, + hotkey_ss58=alice_wallet.hotkey.ss58_address, + take=0.15, + raise_error=True, + ) + + take = await async_subtensor.delegates.get_delegate_take( + alice_wallet.hotkey.ss58_address + ) + assert take == 0.14999618524452582 + + logging.console.success("Test [green]test_change_take_async[/green] passed.") + + +def test_delegates(subtensor, alice_wallet, bob_wallet): + """ + Tests: + - Check default Delegates + - Register Delegates + - Check if Hotkey is a Delegate + - Nominator Staking + """ + logging.console.info("Testing [green]test_delegates[/green].") + + assert subtensor.delegates.get_delegates() == [] + assert subtensor.delegates.get_delegated(alice_wallet.coldkey.ss58_address) == [] + assert ( + subtensor.delegates.get_delegate_by_hotkey(alice_wallet.hotkey.ss58_address) + is None + ) + assert ( + subtensor.delegates.get_delegate_by_hotkey(bob_wallet.hotkey.ss58_address) + is None + ) + + assert ( + subtensor.delegates.is_hotkey_delegate(alice_wallet.hotkey.ss58_address) + is False + ) + assert ( + subtensor.delegates.is_hotkey_delegate(bob_wallet.hotkey.ss58_address) is False + ) + + subtensor.extrinsics.root_register( + alice_wallet, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + subtensor.extrinsics.root_register( + bob_wallet, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + assert ( + subtensor.delegates.is_hotkey_delegate(alice_wallet.hotkey.ss58_address) is True + ) + assert ( + subtensor.delegates.is_hotkey_delegate(bob_wallet.hotkey.ss58_address) is True + ) + + alice_delegate = subtensor.delegates.get_delegate_by_hotkey( + alice_wallet.hotkey.ss58_address + ) + + assert alice_delegate == DelegateInfo( + hotkey_ss58=alice_wallet.hotkey.ss58_address, + owner_ss58=alice_wallet.coldkey.ss58_address, + take=DEFAULT_DELEGATE_TAKE, + validator_permits=[], + registrations=[0], + return_per_1000=Balance(0), + total_daily_return=Balance(0), + total_stake={}, + nominators={}, + ) + + bob_delegate = subtensor.delegates.get_delegate_by_hotkey( + bob_wallet.hotkey.ss58_address + ) + + assert bob_delegate == DelegateInfo( + hotkey_ss58=bob_wallet.hotkey.ss58_address, + owner_ss58=bob_wallet.coldkey.ss58_address, + take=DEFAULT_DELEGATE_TAKE, + validator_permits=[], + registrations=[0], + return_per_1000=Balance(0), + total_daily_return=Balance(0), + total_stake={}, + nominators={}, + ) + + delegates = subtensor.delegates.get_delegates() + + assert delegates == [ + bob_delegate, + alice_delegate, + ] + + assert subtensor.delegates.get_delegated(bob_wallet.coldkey.ss58_address) == [] + + alice_subnet_netuid = subtensor.subnets.get_total_subnets() # 2 + set_tempo = 10 + # Register a subnet, netuid 2 + assert subtensor.subnets.register_subnet(alice_wallet), "Subnet wasn't created" + + # Verify subnet created successfully + assert subtensor.subnets.subnet_exists(alice_subnet_netuid), ( + "Subnet wasn't created successfully" + ) + + assert wait_to_start_call(subtensor, alice_wallet, alice_subnet_netuid) + + # set the same tempo for both type of nodes (fast and non-fast blocks) + assert ( + sudo_set_admin_utils( + substrate=subtensor.substrate, + wallet=alice_wallet, + call_function="sudo_set_tempo", + call_params={"netuid": alice_subnet_netuid, "tempo": set_tempo}, + )[0] + is True + ) + + subtensor.staking.add_stake( + wallet=bob_wallet, + netuid=alice_subnet_netuid, + hotkey_ss58=alice_wallet.hotkey.ss58_address, + amount=Balance.from_tao(10_000), + ) + + # let chain update validator_permits + subtensor.wait_for_block(subtensor.block + set_tempo + 1) + + bob_delegated = subtensor.delegates.get_delegated(bob_wallet.coldkey.ss58_address) + assert bob_delegated == [ + DelegatedInfo( + hotkey_ss58=alice_wallet.hotkey.ss58_address, + owner_ss58=alice_wallet.coldkey.ss58_address, + take=DEFAULT_DELEGATE_TAKE, + validator_permits=[alice_subnet_netuid], + registrations=[0, alice_subnet_netuid], + return_per_1000=Balance(0), + total_daily_return=get_dynamic_balance( + bob_delegated[0].total_daily_return.rao + ), + netuid=alice_subnet_netuid, + stake=get_dynamic_balance(bob_delegated[0].stake.rao, alice_subnet_netuid), + ), + ] + logging.console.success("Test [green]test_delegates[/green] passed.") + + +@pytest.mark.asyncio +async def test_delegates_async(async_subtensor, alice_wallet, bob_wallet): + """ + Tests: + - Check default Delegates + - Register Delegates + - Check if Hotkey is a Delegate + - Nominator Staking + """ + logging.console.info("Testing [green]test_delegates_async[/green].") + + assert await async_subtensor.delegates.get_delegates() == [] + assert ( + await async_subtensor.delegates.get_delegated(alice_wallet.coldkey.ss58_address) + == [] + ) + assert ( + await async_subtensor.delegates.get_delegate_by_hotkey( + alice_wallet.hotkey.ss58_address + ) + is None + ) + assert ( + await async_subtensor.delegates.get_delegate_by_hotkey( + bob_wallet.hotkey.ss58_address + ) + is None + ) + + assert ( + await async_subtensor.delegates.is_hotkey_delegate( + alice_wallet.hotkey.ss58_address + ) + is False + ) + assert ( + await async_subtensor.delegates.is_hotkey_delegate( + bob_wallet.hotkey.ss58_address + ) + is False + ) + + await async_subtensor.extrinsics.root_register( + alice_wallet, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + await async_subtensor.extrinsics.root_register( + bob_wallet, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + assert ( + await async_subtensor.delegates.is_hotkey_delegate( + alice_wallet.hotkey.ss58_address + ) + is True + ) + assert ( + await async_subtensor.delegates.is_hotkey_delegate( + bob_wallet.hotkey.ss58_address + ) + is True + ) + + alice_delegate = await async_subtensor.delegates.get_delegate_by_hotkey( + alice_wallet.hotkey.ss58_address + ) + + assert alice_delegate == DelegateInfo( + hotkey_ss58=alice_wallet.hotkey.ss58_address, + owner_ss58=alice_wallet.coldkey.ss58_address, + take=DEFAULT_DELEGATE_TAKE, + validator_permits=[], + registrations=[0], + return_per_1000=Balance(0), + total_daily_return=Balance(0), + total_stake={}, + nominators={}, + ) + + bob_delegate = await async_subtensor.delegates.get_delegate_by_hotkey( + bob_wallet.hotkey.ss58_address + ) + + assert bob_delegate == DelegateInfo( + hotkey_ss58=bob_wallet.hotkey.ss58_address, + owner_ss58=bob_wallet.coldkey.ss58_address, + take=DEFAULT_DELEGATE_TAKE, + validator_permits=[], + registrations=[0], + return_per_1000=Balance(0), + total_daily_return=Balance(0), + total_stake={}, + nominators={}, + ) + + delegates = await async_subtensor.delegates.get_delegates() + + assert delegates == [ + bob_delegate, + alice_delegate, + ] + + assert ( + await async_subtensor.delegates.get_delegated(bob_wallet.coldkey.ss58_address) + == [] + ) + + alice_subnet_netuid = await async_subtensor.subnets.get_total_subnets() # 2 + set_tempo = 10 + # Register a subnet, netuid 2 + assert await async_subtensor.subnets.register_subnet(alice_wallet), ( + "Subnet wasn't created" + ) + + # Verify subnet created successfully + assert await async_subtensor.subnets.subnet_exists(alice_subnet_netuid), ( + "Subnet wasn't created successfully" + ) + + assert await async_wait_to_start_call( + async_subtensor, alice_wallet, alice_subnet_netuid + ) + + # set the same tempo for both type of nodes (fast and non-fast blocks) + assert ( + await async_sudo_set_admin_utils( + substrate=async_subtensor.substrate, + wallet=alice_wallet, + call_function="sudo_set_tempo", + call_params={"netuid": alice_subnet_netuid, "tempo": set_tempo}, + ) + )[0] is True + + await async_subtensor.staking.add_stake( + wallet=bob_wallet, + netuid=alice_subnet_netuid, + hotkey_ss58=alice_wallet.hotkey.ss58_address, + amount=Balance.from_tao(10_000), + ) + + # let chain update validator_permits + await async_subtensor.wait_for_block(await async_subtensor.block + set_tempo + 1) + + bob_delegated = await async_subtensor.delegates.get_delegated( + bob_wallet.coldkey.ss58_address + ) + assert bob_delegated == [ + DelegatedInfo( + hotkey_ss58=alice_wallet.hotkey.ss58_address, + owner_ss58=alice_wallet.coldkey.ss58_address, + take=DEFAULT_DELEGATE_TAKE, + validator_permits=[alice_subnet_netuid], + registrations=[0, alice_subnet_netuid], + return_per_1000=Balance(0), + total_daily_return=get_dynamic_balance( + bob_delegated[0].total_daily_return.rao + ), + netuid=alice_subnet_netuid, + stake=get_dynamic_balance(bob_delegated[0].stake.rao, alice_subnet_netuid), + ), + ] + logging.console.success("Test [green]test_delegates_async[/green] passed.") + + +def test_nominator_min_required_stake(subtensor, alice_wallet, bob_wallet, dave_wallet): + """ + Tests: + - Check default NominatorMinRequiredStake + - Add Stake to Nominate from Dave to Bob + - Update NominatorMinRequiredStake + - Check Nominator is removed + """ + logging.console.info("Testing [green]test_delegates_async[/green].") + + alice_subnet_netuid = subtensor.subnets.get_total_subnets() # 2 + + # Register a subnet, netuid 2 + assert subtensor.subnets.register_subnet(alice_wallet), "Subnet wasn't created" + + # Verify subnet created successfully + assert subtensor.subnets.subnet_exists(alice_subnet_netuid), ( + "Subnet wasn't created successfully" + ) + assert wait_to_start_call(subtensor, alice_wallet, alice_subnet_netuid) + + minimum_required_stake = subtensor.staking.get_minimum_required_stake() + assert minimum_required_stake == Balance(0) + + subtensor.subnets.burned_register( + wallet=bob_wallet, + netuid=alice_subnet_netuid, + ) + + subtensor.subnets.burned_register( + wallet=dave_wallet, + netuid=alice_subnet_netuid, + ) + + success = subtensor.staking.add_stake( + wallet=dave_wallet, + netuid=alice_subnet_netuid, + hotkey_ss58=bob_wallet.hotkey.ss58_address, + amount=Balance.from_tao(1000), + ) + assert success is True + + stake = subtensor.staking.get_stake( + coldkey_ss58=dave_wallet.coldkey.ss58_address, + hotkey_ss58=bob_wallet.hotkey.ss58_address, + netuid=alice_subnet_netuid, + ) + assert stake > 0 + + # this will trigger clear_small_nominations + sudo_set_admin_utils( + substrate=subtensor.substrate, + wallet=alice_wallet, + call_function="sudo_set_nominator_min_required_stake", + call_params={ + "min_stake": "100000000000000", + }, + ) + + minimum_required_stake = subtensor.staking.get_minimum_required_stake() + assert minimum_required_stake == Balance.from_tao(100_000) + + stake = subtensor.staking.get_stake( + coldkey_ss58=dave_wallet.coldkey.ss58_address, + hotkey_ss58=bob_wallet.hotkey.ss58_address, + netuid=alice_subnet_netuid, + ) + assert stake == Balance(0) + + logging.console.success( + "Test [green]test_nominator_min_required_stake[/green] passed." + ) + + +@pytest.mark.asyncio +async def test_nominator_min_required_stake_async( + async_subtensor, alice_wallet, bob_wallet, dave_wallet +): + """ + Async tests: + - Check default NominatorMinRequiredStake + - Add Stake to Nominate from Dave to Bob + - Update NominatorMinRequiredStake + - Check Nominator is removed + """ + logging.console.info( + "Testing [green]test_nominator_min_required_stake_async[/green]." + ) + + alice_subnet_netuid = await async_subtensor.subnets.get_total_subnets() # 2 + + # Register a subnet, netuid 2 + assert await async_subtensor.subnets.register_subnet(alice_wallet), ( + "Subnet wasn't created" + ) + + # Verify subnet created successfully + assert await async_subtensor.subnets.subnet_exists(alice_subnet_netuid), ( + "Subnet wasn't created successfully" + ) + assert await async_wait_to_start_call( + async_subtensor, alice_wallet, alice_subnet_netuid + ) + + minimum_required_stake = await async_subtensor.staking.get_minimum_required_stake() + assert minimum_required_stake == Balance(0) + + await async_subtensor.subnets.burned_register( + wallet=bob_wallet, + netuid=alice_subnet_netuid, + ) + + await async_subtensor.subnets.burned_register( + wallet=dave_wallet, + netuid=alice_subnet_netuid, + ) + + success = await async_subtensor.staking.add_stake( + wallet=dave_wallet, + netuid=alice_subnet_netuid, + hotkey_ss58=bob_wallet.hotkey.ss58_address, + amount=Balance.from_tao(1000), + ) + assert success is True + + stake = await async_subtensor.staking.get_stake( + coldkey_ss58=dave_wallet.coldkey.ss58_address, + hotkey_ss58=bob_wallet.hotkey.ss58_address, + netuid=alice_subnet_netuid, + ) + assert stake > 0 + + # this will trigger clear_small_nominations + await async_sudo_set_admin_utils( + substrate=async_subtensor.substrate, + wallet=alice_wallet, + call_function="sudo_set_nominator_min_required_stake", + call_params={ + "min_stake": "100000000000000", + }, + ) + + minimum_required_stake = await async_subtensor.staking.get_minimum_required_stake() + assert minimum_required_stake == Balance.from_tao(100_000) + + stake = await async_subtensor.staking.get_stake( + coldkey_ss58=dave_wallet.coldkey.ss58_address, + hotkey_ss58=bob_wallet.hotkey.ss58_address, + netuid=alice_subnet_netuid, + ) + assert stake == Balance(0) + + logging.console.success( + "Test [green]test_nominator_min_required_stake_async[/green] passed." + ) + + +def test_get_vote_data(subtensor, alice_wallet): + """ + Tests: + - Sends Propose + - Checks existing Proposals + - Votes + - Checks Proposal is updated + """ + logging.console.info("Testing [green]test_get_vote_data[/green].") + + assert subtensor.extrinsics.root_register(alice_wallet), ( + "Can not register Alice in root SN." + ) + + proposals = subtensor.queries.query_map( + "Triumvirate", + "ProposalOf", + params=[], + ) + + assert proposals.records == [] + + success, error = propose( + subtensor=subtensor, + wallet=alice_wallet, + proposal=subtensor.substrate.compose_call( + call_module="Triumvirate", + call_function="set_members", + call_params={ + "new_members": [], + "prime": None, + "old_count": 0, + }, + ), + duration=1_000_000, + ) + + assert error == "" + assert success is True + + proposals = subtensor.queries.query_map( + module="Triumvirate", + name="ProposalOf", + params=[], + ) + proposals = { + bytes(proposal_hash[0]): proposal.value for proposal_hash, proposal in proposals + } + + assert list(proposals.values()) == [ + { + "Triumvirate": ( + { + "set_members": { + "new_members": (), + "prime": None, + "old_count": 0, + }, + }, + ), + }, + ] + + proposal_hash = list(proposals.keys())[0] + proposal_hash = f"0x{proposal_hash.hex()}" + + proposal = subtensor.chain.get_vote_data( + proposal_hash, + ) + + assert proposal == ProposalVoteData( + ayes=[], + end=CloseInValue(1_000_000, subtensor.block), + index=0, + nays=[], + threshold=3, + ) + + success, error = vote( + subtensor=subtensor, + wallet=alice_wallet, + hotkey=alice_wallet.hotkey.ss58_address, + proposal=proposal_hash, + index=0, + approve=True, + ) + + assert error == "" + assert success is True + + proposal = subtensor.chain.get_vote_data( + proposal_hash=proposal_hash, + ) + + assert proposal == ProposalVoteData( + ayes=[ + alice_wallet.hotkey.ss58_address, + ], + end=CloseInValue(1_000_000, subtensor.block), + index=0, + nays=[], + threshold=3, + ) + logging.console.success("Test [green]test_get_vote_data[/green] passed.") + + +@pytest.mark.asyncio +async def test_get_vote_data_async(async_subtensor, alice_wallet): + """ + Async tests: + - Sends Propose + - Checks existing Proposals + - Votes + - Checks Proposal is updated + """ + logging.console.info("Testing [green]test_get_vote_data_async[/green].") + + assert await async_subtensor.extrinsics.root_register(alice_wallet), ( + "Can not register Alice in root SN." + ) + + proposals = await async_subtensor.queries.query_map( + "Triumvirate", + "ProposalOf", + params=[], + ) + + assert proposals.records == [] + + success, error = await async_propose( + subtensor=async_subtensor, + wallet=alice_wallet, + proposal=await async_subtensor.substrate.compose_call( + call_module="Triumvirate", + call_function="set_members", + call_params={ + "new_members": [], + "prime": None, + "old_count": 0, + }, + ), + duration=1_000_000, + ) + + assert error == "" + assert success is True + + proposals = await async_subtensor.queries.query_map( + module="Triumvirate", + name="ProposalOf", + params=[], + ) + proposals = { + bytes(proposal_hash[0]): proposal.value + async for proposal_hash, proposal in proposals + } + + assert list(proposals.values()) == [ + { + "Triumvirate": ( + { + "set_members": { + "new_members": (), + "prime": None, + "old_count": 0, + }, + }, + ), + }, + ] + + proposal_hash = list(proposals.keys())[0] + proposal_hash = f"0x{proposal_hash.hex()}" + + proposal = await async_subtensor.chain.get_vote_data( + proposal_hash, + ) + + assert proposal == ProposalVoteData( + ayes=[], + end=CloseInValue(1_000_000, await async_subtensor.block), + index=0, + nays=[], + threshold=3, + ) + + success, error = await async_vote( + subtensor=async_subtensor, + wallet=alice_wallet, + hotkey=alice_wallet.hotkey.ss58_address, + proposal=proposal_hash, + index=0, + approve=True, + ) + + assert error == "" + assert success is True + + proposal = await async_subtensor.chain.get_vote_data( + proposal_hash=proposal_hash, + ) + + assert proposal == ProposalVoteData( + ayes=[ + alice_wallet.hotkey.ss58_address, + ], + end=CloseInValue(1_000_000, await async_subtensor.block), + index=0, + nays=[], + threshold=3, + ) + logging.console.success("Test [green]test_get_vote_data_async[/green] passed.") diff --git a/tests/e2e_tests/test_dendrite.py b/tests/e2e_tests/test_dendrite.py new file mode 100644 index 0000000000..73d646aadf --- /dev/null +++ b/tests/e2e_tests/test_dendrite.py @@ -0,0 +1,316 @@ +import asyncio + +import pytest + +from bittensor.utils.balance import Balance +from bittensor.utils.btlogging import logging +from tests.e2e_tests.utils.chain_interactions import ( + async_sudo_set_admin_utils, + async_wait_epoch, + sudo_set_admin_utils, + wait_epoch, +) +from tests.e2e_tests.utils.e2e_test_utils import ( + async_wait_to_start_call, + wait_to_start_call, +) + +logging.on() +logging.set_debug() + +NON_FAST_RUNTIME_TEMPO = 10 + + +@pytest.mark.asyncio +async def test_dendrite(subtensor, templates, alice_wallet, bob_wallet): + """ + Test the Dendrite mechanism + + Steps: + 1. Register a subnet through Alice + 2. Register Bob as a validator + 3. Add stake to Bob and ensure neuron is not a validator yet + 4. Run Bob as a validator and wait epoch + 5. Ensure Bob's neuron has all correct attributes of a validator + Raises: + AssertionError: If any of the checks or verifications fail + """ + logging.console.info("Testing `test_dendrite`.") + + alice_subnet_netuid = subtensor.subnets.get_total_subnets() # 2 + + # Register a subnet, netuid 2 + assert subtensor.subnets.register_subnet(alice_wallet), "Subnet wasn't created." + + # Verify subnet created successfully + assert subtensor.subnets.subnet_exists(alice_subnet_netuid), ( + "Subnet wasn't created successfully." + ) + + assert wait_to_start_call(subtensor, alice_wallet, alice_subnet_netuid), ( + "Subnet wasn't started." + ) + assert subtensor.subnets.is_subnet_active(alice_subnet_netuid), ( + "Subnet is not active." + ) + + if not subtensor.chain.is_fast_blocks(): + # Make sure Alice is Top Validator (for non-fast-runtime only) + assert subtensor.staking.add_stake( + wallet=alice_wallet, + netuid=alice_subnet_netuid, + hotkey_ss58=alice_wallet.hotkey.ss58_address, + amount=Balance.from_tao(1), + ) + # set tempo to 10 block for non-fast-runtime + assert sudo_set_admin_utils( + substrate=subtensor.substrate, + wallet=alice_wallet, + call_function="sudo_set_tempo", + call_params={ + "netuid": alice_subnet_netuid, + "tempo": NON_FAST_RUNTIME_TEMPO, + }, + ) + + # update max_allowed_validators so only one neuron can get validator_permit + assert sudo_set_admin_utils( + substrate=subtensor.substrate, + wallet=alice_wallet, + call_function="sudo_set_max_allowed_validators", + call_params={ + "netuid": alice_subnet_netuid, + "max_allowed_validators": 1, + }, + ) + + # update weights_set_rate_limit for fast-blocks + status, error = sudo_set_admin_utils( + substrate=subtensor.substrate, + wallet=alice_wallet, + call_function="sudo_set_weights_set_rate_limit", + call_params={ + "netuid": alice_subnet_netuid, + "weights_set_rate_limit": 10, + }, + ) + assert error is None + assert status is True + + # Register Bob to the network + assert subtensor.subnets.burned_register(bob_wallet, alice_subnet_netuid), ( + "Unable to register Bob as a neuron" + ) + + metagraph = subtensor.metagraphs.metagraph(alice_subnet_netuid) + + # Assert neurons are Alice and Bob + assert len(metagraph.neurons) == 2 + + alice_neuron = metagraph.neurons[0] + assert alice_neuron.hotkey == alice_wallet.hotkey.ss58_address + assert alice_neuron.coldkey == alice_wallet.coldkey.ss58_address + + bob_neuron = metagraph.neurons[1] + assert bob_neuron.hotkey == bob_wallet.hotkey.ss58_address + assert bob_neuron.coldkey == bob_wallet.coldkey.ss58_address + + # Assert stake is 0 + assert bob_neuron.stake.tao == 0 + + # Stake to become to top neuron after the first epoch + tao = Balance.from_tao(10_000) + alpha, _ = subtensor.subnets.subnet(alice_subnet_netuid).tao_to_alpha_with_slippage( + tao + ) + + assert subtensor.staking.add_stake( + wallet=bob_wallet, + netuid=alice_subnet_netuid, + hotkey_ss58=bob_wallet.hotkey.ss58_address, + amount=tao, + ) + + # Refresh metagraph + metagraph = subtensor.metagraphs.metagraph(alice_subnet_netuid) + bob_neuron = metagraph.neurons[1] + + # Assert alpha is close to stake equivalent + assert 0.95 < bob_neuron.stake.rao / alpha.rao < 1.05 + + # Assert neuron is not a validator yet + assert bob_neuron.active is True + assert bob_neuron.validator_permit is False + assert bob_neuron.validator_trust == 0.0 + assert bob_neuron.pruning_score == 0 + + async with templates.validator(bob_wallet, alice_subnet_netuid): + await asyncio.sleep(5) # wait for 5 seconds for the Validator to process + + await wait_epoch(subtensor, netuid=alice_subnet_netuid) + + # Refresh metagraph + metagraph = subtensor.metagraphs.metagraph(alice_subnet_netuid) + + # Refresh validator neuron + updated_neuron = metagraph.neurons[1] + + assert len(metagraph.neurons) == 2 + assert updated_neuron.active is True + assert updated_neuron.validator_permit is True + assert updated_neuron.hotkey == bob_wallet.hotkey.ss58_address + assert updated_neuron.coldkey == bob_wallet.coldkey.ss58_address + assert updated_neuron.pruning_score != 0 + + logging.console.info("✅ Passed `test_dendrite`") + + +@pytest.mark.asyncio +async def test_dendrite_async(async_subtensor, templates, alice_wallet, bob_wallet): + """ + Test the Dendrite mechanism + + Steps: + 1. Register a subnet through Alice + 2. Register Bob as a validator + 3. Add stake to Bob and ensure neuron is not a validator yet + 4. Run Bob as a validator and wait epoch + 5. Ensure Bob's neuron has all correct attributes of a validator + Raises: + AssertionError: If any of the checks or verifications fail + """ + logging.console.info("Testing `test_dendrite_async`.") + + alice_subnet_netuid = await async_subtensor.subnets.get_total_subnets() # 2 + + # Register a subnet, netuid 2 + assert await async_subtensor.subnets.register_subnet(alice_wallet), ( + "Subnet wasn't created" + ) + + # Verify subnet created successfully + assert await async_subtensor.subnets.subnet_exists(alice_subnet_netuid), ( + "Subnet wasn't created successfully" + ) + + assert await async_wait_to_start_call( + async_subtensor, alice_wallet, alice_subnet_netuid + ), "Subnet wasn't started." + + assert await async_subtensor.subnets.is_subnet_active(alice_subnet_netuid), ( + "Subnet is not active." + ) + + if not await async_subtensor.chain.is_fast_blocks(): + # Make sure Alice is Top Validator (for non-fast-runtime only) + assert await async_subtensor.staking.add_stake( + wallet=alice_wallet, + netuid=alice_subnet_netuid, + hotkey_ss58=alice_wallet.hotkey.ss58_address, + amount=Balance.from_tao(5), + wait_for_inclusion=False, + wait_for_finalization=False, + ) + # set tempo to 10 block for non-fast-runtime + assert await async_sudo_set_admin_utils( + substrate=async_subtensor.substrate, + wallet=alice_wallet, + call_function="sudo_set_tempo", + call_params={ + "netuid": alice_subnet_netuid, + "tempo": NON_FAST_RUNTIME_TEMPO, + }, + ) + + # update max_allowed_validators so only one neuron can get validator_permit + assert await async_sudo_set_admin_utils( + substrate=async_subtensor.substrate, + wallet=alice_wallet, + call_function="sudo_set_max_allowed_validators", + call_params={ + "netuid": alice_subnet_netuid, + "max_allowed_validators": 1, + }, + ) + + # update weights_set_rate_limit for fast-blocks + status, error = await async_sudo_set_admin_utils( + substrate=async_subtensor.substrate, + wallet=alice_wallet, + call_function="sudo_set_weights_set_rate_limit", + call_params={ + "netuid": alice_subnet_netuid, + "weights_set_rate_limit": 10, + }, + ) + assert error is None + assert status is True + + # Register Bob to the network + assert await async_subtensor.subnets.burned_register( + bob_wallet, alice_subnet_netuid + ), "Unable to register Bob as a neuron" + + metagraph = await async_subtensor.metagraphs.metagraph(alice_subnet_netuid) + + # Assert neurons are Alice and Bob + assert len(metagraph.neurons) == 2 + + alice_neuron = metagraph.neurons[0] + assert alice_neuron.hotkey == alice_wallet.hotkey.ss58_address + assert alice_neuron.coldkey == alice_wallet.coldkey.ss58_address + + bob_neuron = metagraph.neurons[1] + assert bob_neuron.hotkey == bob_wallet.hotkey.ss58_address + assert bob_neuron.coldkey == bob_wallet.coldkey.ss58_address + + # Assert stake is 0 + assert bob_neuron.stake.tao == 0 + + # Stake to become to top neuron after the first epoch + tao = Balance.from_tao(10_000) + alpha, _ = ( + await async_subtensor.subnets.subnet(alice_subnet_netuid) + ).tao_to_alpha_with_slippage(tao) + + assert await async_subtensor.staking.add_stake( + wallet=bob_wallet, + netuid=alice_subnet_netuid, + hotkey_ss58=bob_wallet.hotkey.ss58_address, + amount=tao, + wait_for_inclusion=False, + wait_for_finalization=False, + ) + + # Refresh metagraph + metagraph = await async_subtensor.metagraphs.metagraph(alice_subnet_netuid) + bob_neuron = metagraph.neurons[1] + + # Assert alpha is close to stake equivalent + assert 0.95 < bob_neuron.stake.rao / alpha.rao < 1.05 + + # Assert neuron is not a validator yet + assert bob_neuron.active is True + assert bob_neuron.validator_permit is False + assert bob_neuron.validator_trust == 0.0 + assert bob_neuron.pruning_score == 0 + + async with templates.validator(bob_wallet, alice_subnet_netuid): + await asyncio.sleep(5) # wait for 5 seconds for the Validator to process + + await async_wait_epoch(async_subtensor, netuid=alice_subnet_netuid) + + # Refresh metagraph + metagraph = await async_subtensor.metagraphs.metagraph(alice_subnet_netuid) + + # Refresh validator neuron + updated_neuron = metagraph.neurons[1] + + assert len(metagraph.neurons) == 2 + assert updated_neuron.active is True + assert updated_neuron.validator_permit is True + assert updated_neuron.hotkey == bob_wallet.hotkey.ss58_address + assert updated_neuron.coldkey == bob_wallet.coldkey.ss58_address + assert updated_neuron.pruning_score != 0 + + logging.console.info("✅ Passed `test_dendrite_async`") diff --git a/tests/e2e_tests/test_hotkeys.py b/tests/e2e_tests/test_hotkeys.py new file mode 100644 index 0000000000..f9fde7a37f --- /dev/null +++ b/tests/e2e_tests/test_hotkeys.py @@ -0,0 +1,789 @@ +import pytest + +from bittensor.core.errors import ( + NotEnoughStakeToSetChildkeys, + RegistrationNotPermittedOnRootSubnet, + SubNetworkDoesNotExist, + InvalidChild, + TooManyChildren, + ProportionOverflow, + DuplicateChild, + TxRateLimitExceeded, + NonAssociatedColdKey, +) +from bittensor.utils.btlogging import logging +from tests.e2e_tests.utils.chain_interactions import ( + async_sudo_set_admin_utils, + sudo_set_admin_utils, +) +from tests.e2e_tests.utils.e2e_test_utils import ( + async_wait_to_start_call, + wait_to_start_call, +) + +SET_CHILDREN_RATE_LIMIT = 30 +ROOT_COOLDOWN = 50 # blocks + + +def test_hotkeys(subtensor, alice_wallet, dave_wallet): + """ + Tests: + - Check if Hotkey exists + - Check if Hotkey is registered + """ + logging.console.info("Testing [green]test_hotkeys[/green].") + + dave_subnet_netuid = subtensor.subnets.get_total_subnets() # 2 + assert subtensor.subnets.register_subnet(dave_wallet) + assert subtensor.subnets.subnet_exists(dave_subnet_netuid), ( + f"Subnet #{dave_subnet_netuid} does not exist." + ) + + assert wait_to_start_call(subtensor, dave_wallet, dave_subnet_netuid) + + coldkey = alice_wallet.coldkeypub.ss58_address + hotkey = alice_wallet.hotkey.ss58_address + + with pytest.raises(ValueError, match="Invalid checksum"): + subtensor.wallets.does_hotkey_exist("fake") + + assert subtensor.wallets.does_hotkey_exist(hotkey) is False + assert subtensor.wallets.get_hotkey_owner(hotkey) is None + + assert subtensor.wallets.is_hotkey_registered(hotkey) is False + assert subtensor.wallets.is_hotkey_registered_any(hotkey) is False + assert ( + subtensor.wallets.is_hotkey_registered_on_subnet( + hotkey_ss58=hotkey, + netuid=dave_subnet_netuid, + ) + is False + ) + + assert subtensor.subnets.burned_register( + wallet=alice_wallet, + netuid=dave_subnet_netuid, + ) + + assert subtensor.wallets.does_hotkey_exist(hotkey) is True + assert subtensor.wallets.get_hotkey_owner(hotkey) == coldkey + + assert subtensor.wallets.is_hotkey_registered(hotkey) is True + assert subtensor.wallets.is_hotkey_registered_any(hotkey) is True + assert ( + subtensor.wallets.is_hotkey_registered_on_subnet( + hotkey_ss58=hotkey, + netuid=dave_subnet_netuid, + ) + is True + ) + logging.console.success("✅ Test [green]test_hotkeys[/green] passed") + + +@pytest.mark.asyncio +async def test_hotkeys_async(async_subtensor, alice_wallet, dave_wallet): + """ + Async tests: + - Check if Hotkey exists + - Check if Hotkey is registered + """ + logging.console.info("Testing [green]test_hotkeys_async[/green].") + + dave_subnet_netuid = await async_subtensor.subnets.get_total_subnets() # 2 + assert await async_subtensor.subnets.register_subnet(dave_wallet) + assert await async_subtensor.subnets.subnet_exists(dave_subnet_netuid), ( + f"Subnet #{dave_subnet_netuid} does not exist." + ) + + assert await async_wait_to_start_call( + async_subtensor, dave_wallet, dave_subnet_netuid + ) + + coldkey = alice_wallet.coldkeypub.ss58_address + hotkey = alice_wallet.hotkey.ss58_address + + with pytest.raises(ValueError, match="Invalid checksum"): + await async_subtensor.wallets.does_hotkey_exist("fake") + + assert await async_subtensor.wallets.does_hotkey_exist(hotkey) is False + assert await async_subtensor.wallets.get_hotkey_owner(hotkey) is None + + assert await async_subtensor.wallets.is_hotkey_registered(hotkey) is False + assert await async_subtensor.wallets.is_hotkey_registered_any(hotkey) is False + assert ( + await async_subtensor.wallets.is_hotkey_registered_on_subnet( + hotkey_ss58=hotkey, + netuid=dave_subnet_netuid, + ) + is False + ) + + assert await async_subtensor.subnets.burned_register( + wallet=alice_wallet, + netuid=dave_subnet_netuid, + ) + + assert await async_subtensor.wallets.does_hotkey_exist(hotkey) is True + assert await async_subtensor.wallets.get_hotkey_owner(hotkey) == coldkey + + assert await async_subtensor.wallets.is_hotkey_registered(hotkey) is True + assert await async_subtensor.wallets.is_hotkey_registered_any(hotkey) is True + assert ( + await async_subtensor.wallets.is_hotkey_registered_on_subnet( + hotkey_ss58=hotkey, + netuid=dave_subnet_netuid, + ) + is True + ) + logging.console.success("✅ Test [green]test_hotkeys[/green] passed") + + +def test_children(subtensor, alice_wallet, bob_wallet, dave_wallet): + """ + Tests: + - Get default children (empty list) + - Call `root_set_pending_childkey_cooldown` extrinsic. + - Update children list + - Checking pending children + - Checking cooldown period + - Trigger rate limit + - Clear children list + """ + + logging.console.info("Testing [green]test_children[/green].") + + dave_subnet_netuid = subtensor.subnets.get_total_subnets() # 2 + set_tempo = 10 # affect to non-fast-blocks mode + + # Set cooldown + success, message = subtensor.extrinsics.root_set_pending_childkey_cooldown( + wallet=alice_wallet, cooldown=ROOT_COOLDOWN + ) + assert success, f"Call `root_set_pending_childkey_cooldown` failed: {message}" + assert ( + message + == "Success with `root_set_pending_childkey_cooldown_extrinsic` response." + ) + + assert subtensor.subnets.register_subnet(dave_wallet) + assert subtensor.subnets.subnet_exists(dave_subnet_netuid), ( + f"Subnet #{dave_subnet_netuid} does not exist." + ) + + assert wait_to_start_call(subtensor, dave_wallet, dave_subnet_netuid) + + # set the same tempo for both type of nodes (to avoid tests timeout) + if not subtensor.chain.is_fast_blocks(): + assert ( + sudo_set_admin_utils( + substrate=subtensor.substrate, + wallet=alice_wallet, + call_function="sudo_set_tempo", + call_params={"netuid": dave_subnet_netuid, "tempo": set_tempo}, + )[0] + is True + ) + + assert ( + sudo_set_admin_utils( + substrate=subtensor.substrate, + wallet=alice_wallet, + call_function="sudo_set_tx_rate_limit", + call_params={"tx_rate_limit": 0}, + )[0] + is True + ) + + with pytest.raises(RegistrationNotPermittedOnRootSubnet): + subtensor.extrinsics.set_children( + wallet=alice_wallet, + hotkey=alice_wallet.hotkey.ss58_address, + netuid=0, + children=[], + raise_error=True, + ) + + with pytest.raises(NonAssociatedColdKey): + subtensor.extrinsics.set_children( + wallet=alice_wallet, + hotkey=alice_wallet.hotkey.ss58_address, + netuid=1, + children=[], + raise_error=True, + ) + + with pytest.raises(SubNetworkDoesNotExist): + subtensor.extrinsics.set_children( + wallet=alice_wallet, + hotkey=alice_wallet.hotkey.ss58_address, + netuid=3, + children=[], + raise_error=True, + ) + + assert subtensor.subnets.burned_register( + wallet=alice_wallet, + netuid=dave_subnet_netuid, + ) + logging.console.success(f"Alice registered on subnet {dave_subnet_netuid}") + + assert subtensor.subnets.burned_register( + wallet=bob_wallet, + netuid=dave_subnet_netuid, + ) + logging.console.success(f"Bob registered on subnet {dave_subnet_netuid}") + + success, children, error = subtensor.wallets.get_children( + hotkey=alice_wallet.hotkey.ss58_address, + netuid=dave_subnet_netuid, + ) + + assert error == "" + assert success is True + assert children == [] + + with pytest.raises(InvalidChild): + subtensor.extrinsics.set_children( + wallet=alice_wallet, + hotkey=alice_wallet.hotkey.ss58_address, + netuid=dave_subnet_netuid, + children=[ + ( + 1.0, + alice_wallet.hotkey.ss58_address, + ), + ], + raise_error=True, + ) + + with pytest.raises(TooManyChildren): + subtensor.extrinsics.set_children( + wallet=alice_wallet, + hotkey=alice_wallet.hotkey.ss58_address, + netuid=dave_subnet_netuid, + children=[ + ( + 0.1, + bob_wallet.hotkey.ss58_address, + ) + for _ in range(10) + ], + raise_error=True, + ) + + with pytest.raises(ProportionOverflow): + subtensor.extrinsics.set_children( + wallet=alice_wallet, + hotkey=alice_wallet.hotkey.ss58_address, + netuid=dave_subnet_netuid, + children=[ + ( + 1.0, + bob_wallet.hotkey.ss58_address, + ), + ( + 1.0, + "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM", + ), + ], + raise_error=True, + ) + + with pytest.raises(DuplicateChild): + subtensor.extrinsics.set_children( + wallet=alice_wallet, + hotkey=alice_wallet.hotkey.ss58_address, + netuid=dave_subnet_netuid, + children=[ + ( + 0.5, + bob_wallet.hotkey.ss58_address, + ), + ( + 0.5, + bob_wallet.hotkey.ss58_address, + ), + ], + raise_error=True, + ) + + success, message = subtensor.extrinsics.set_children( + wallet=alice_wallet, + hotkey=alice_wallet.hotkey.ss58_address, + netuid=dave_subnet_netuid, + children=[ + ( + 1.0, + bob_wallet.hotkey.ss58_address, + ), + ], + raise_error=True, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + assert message == "Success with `set_children_extrinsic` response." + assert success is True + + set_children_block = subtensor.block + + # children not set yet (have to wait cool-down period) + success, children, error = subtensor.wallets.get_children( + hotkey=alice_wallet.hotkey.ss58_address, + block=set_children_block, + netuid=dave_subnet_netuid, + ) + + assert success is True + assert children == [] + assert error == "" + + # children are in pending state + pending, cooldown = subtensor.wallets.get_children_pending( + hotkey=alice_wallet.hotkey.ss58_address, + netuid=dave_subnet_netuid, + ) + + logging.console.info( + f"[orange]block: {subtensor.block}, cooldown: {cooldown}[/orange]" + ) + + assert pending == [(1.0, bob_wallet.hotkey.ss58_address)] + + # we use `*2` to ensure that the chain has time to process + subtensor.wait_for_block(cooldown + SET_CHILDREN_RATE_LIMIT * 2) + + success, children, error = subtensor.wallets.get_children( + hotkey=alice_wallet.hotkey.ss58_address, + netuid=dave_subnet_netuid, + ) + + assert error == "" + assert success is True + assert children == [(1.0, bob_wallet.hotkey.ss58_address)] + + parent_ = subtensor.wallets.get_parents( + bob_wallet.hotkey.ss58_address, dave_subnet_netuid + ) + + assert parent_ == [(1.0, alice_wallet.hotkey.ss58_address)] + + # pending queue is empty + pending, cooldown = subtensor.wallets.get_children_pending( + hotkey=alice_wallet.hotkey.ss58_address, + netuid=dave_subnet_netuid, + ) + assert pending == [] + logging.console.info( + f"[orange]block: {subtensor.block}, cooldown: {cooldown}[/orange]" + ) + + with pytest.raises(TxRateLimitExceeded): + set_children_block = subtensor.block + subtensor.extrinsics.set_children( + wallet=alice_wallet, + hotkey=alice_wallet.hotkey.ss58_address, + netuid=dave_subnet_netuid, + children=[], + raise_error=True, + ) + subtensor.extrinsics.set_children( + wallet=alice_wallet, + hotkey=alice_wallet.hotkey.ss58_address, + netuid=dave_subnet_netuid, + children=[], + raise_error=True, + ) + + subtensor.wait_for_block(set_children_block + SET_CHILDREN_RATE_LIMIT) + + success, message = subtensor.extrinsics.set_children( + wallet=alice_wallet, + hotkey=alice_wallet.hotkey.ss58_address, + netuid=dave_subnet_netuid, + children=[], + raise_error=True, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert success, message + + set_children_block = subtensor.block + + pending, cooldown = subtensor.wallets.get_children_pending( + hotkey=alice_wallet.hotkey.ss58_address, + netuid=dave_subnet_netuid, + ) + + assert pending == [] + logging.console.info( + f"[orange]block: {subtensor.block}, cooldown: {cooldown}[/orange]" + ) + + subtensor.wait_for_block(cooldown) + + success, children, error = subtensor.wallets.get_children( + hotkey=alice_wallet.hotkey.ss58_address, + netuid=dave_subnet_netuid, + ) + + assert error == "" + assert success is True + assert children == [(1.0, bob_wallet.hotkey.ss58_address)] + + subtensor.wait_for_block(set_children_block + SET_CHILDREN_RATE_LIMIT) + + sudo_set_admin_utils( + substrate=subtensor.substrate, + wallet=alice_wallet, + call_function="sudo_set_stake_threshold", + call_params={ + "min_stake": 1_000_000_000_000, + }, + ) + + with pytest.raises(NotEnoughStakeToSetChildkeys): + subtensor.extrinsics.set_children( + wallet=alice_wallet, + hotkey=alice_wallet.hotkey.ss58_address, + netuid=dave_subnet_netuid, + children=[ + ( + 1.0, + bob_wallet.hotkey.ss58_address, + ), + ], + raise_error=True, + ) + + logging.console.success(f"✅ Test [green]test_children[/green] passed") + + +@pytest.mark.asyncio +async def test_children_async(async_subtensor, alice_wallet, bob_wallet, dave_wallet): + """ + Async tests: + - Get default children (empty list) + - Call `root_set_pending_childkey_cooldown` extrinsic. + - Update children list + - Checking pending children + - Checking cooldown period + - Trigger rate limit + - Clear children list + """ + + logging.console.info("Testing [green]test_children_async[/green].") + + dave_subnet_netuid = await async_subtensor.subnets.get_total_subnets() # 2 + set_tempo = 10 # affect to non-fast-blocks mode + + # Set cooldown + ( + success, + message, + ) = await async_subtensor.extrinsics.root_set_pending_childkey_cooldown( + wallet=alice_wallet, cooldown=ROOT_COOLDOWN + ) + assert success, f"Call `root_set_pending_childkey_cooldown` failed: {message}" + assert ( + message + == "Success with `root_set_pending_childkey_cooldown_extrinsic` response." + ) + + assert await async_subtensor.subnets.register_subnet(dave_wallet) + assert await async_subtensor.subnets.subnet_exists(dave_subnet_netuid), ( + f"Subnet #{dave_subnet_netuid} does not exist." + ) + + assert ( + await async_wait_to_start_call(async_subtensor, dave_wallet, dave_subnet_netuid) + is True + ) + + # set the same tempo for both type of nodes (to avoid tests timeout) + if not await async_subtensor.chain.is_fast_blocks(): + assert ( + await async_sudo_set_admin_utils( + substrate=async_subtensor.substrate, + wallet=alice_wallet, + call_function="sudo_set_tempo", + call_params={"netuid": dave_subnet_netuid, "tempo": set_tempo}, + ) + )[0] is True + + assert ( + await async_sudo_set_admin_utils( + substrate=async_subtensor.substrate, + wallet=alice_wallet, + call_function="sudo_set_tx_rate_limit", + call_params={"tx_rate_limit": 0}, + ) + )[0] is True + assert await async_subtensor.subnets.tempo(dave_subnet_netuid) == set_tempo + assert await async_subtensor.chain.tx_rate_limit(dave_subnet_netuid) == 0 + + with pytest.raises(RegistrationNotPermittedOnRootSubnet): + await async_subtensor.extrinsics.set_children( + wallet=alice_wallet, + hotkey=alice_wallet.hotkey.ss58_address, + netuid=0, + children=[], + raise_error=True, + ) + + with pytest.raises(NonAssociatedColdKey): + await async_subtensor.extrinsics.set_children( + wallet=alice_wallet, + hotkey=alice_wallet.hotkey.ss58_address, + netuid=1, + children=[], + raise_error=True, + ) + + with pytest.raises(SubNetworkDoesNotExist): + await async_subtensor.extrinsics.set_children( + wallet=alice_wallet, + hotkey=alice_wallet.hotkey.ss58_address, + netuid=3, + children=[], + raise_error=True, + ) + + assert await async_subtensor.subnets.burned_register( + wallet=alice_wallet, + netuid=dave_subnet_netuid, + ) + logging.console.success(f"Alice registered on subnet {dave_subnet_netuid}") + + assert await async_subtensor.subnets.burned_register( + wallet=bob_wallet, + netuid=dave_subnet_netuid, + ) + logging.console.success(f"Bob registered on subnet {dave_subnet_netuid}") + + success, children, error = await async_subtensor.wallets.get_children( + hotkey=alice_wallet.hotkey.ss58_address, + netuid=dave_subnet_netuid, + ) + + assert error == "" + assert success is True + assert children == [] + + with pytest.raises(InvalidChild): + await async_subtensor.extrinsics.set_children( + wallet=alice_wallet, + hotkey=alice_wallet.hotkey.ss58_address, + netuid=dave_subnet_netuid, + children=[ + ( + 1.0, + alice_wallet.hotkey.ss58_address, + ), + ], + raise_error=True, + ) + + with pytest.raises(TooManyChildren): + await async_subtensor.extrinsics.set_children( + wallet=alice_wallet, + hotkey=alice_wallet.hotkey.ss58_address, + netuid=dave_subnet_netuid, + children=[ + ( + 0.1, + bob_wallet.hotkey.ss58_address, + ) + for _ in range(10) + ], + raise_error=True, + ) + + with pytest.raises(ProportionOverflow): + await async_subtensor.extrinsics.set_children( + wallet=alice_wallet, + hotkey=alice_wallet.hotkey.ss58_address, + netuid=dave_subnet_netuid, + children=[ + ( + 1.0, + bob_wallet.hotkey.ss58_address, + ), + ( + 1.0, + "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM", + ), + ], + raise_error=True, + ) + + with pytest.raises(DuplicateChild): + await async_subtensor.extrinsics.set_children( + wallet=alice_wallet, + hotkey=alice_wallet.hotkey.ss58_address, + netuid=dave_subnet_netuid, + children=[ + ( + 0.5, + bob_wallet.hotkey.ss58_address, + ), + ( + 0.5, + bob_wallet.hotkey.ss58_address, + ), + ], + raise_error=True, + ) + + success, message = await async_subtensor.extrinsics.set_children( + wallet=alice_wallet, + hotkey=alice_wallet.hotkey.ss58_address, + netuid=dave_subnet_netuid, + children=[ + ( + 1.0, + bob_wallet.hotkey.ss58_address, + ), + ], + raise_error=True, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert message == "Success with `set_children_extrinsic` response." + assert success is True + logging.console.info(f"[orange]success: {success}, message: {message}[/orange]") + + set_children_block = await async_subtensor.chain.get_current_block() + + # children not set yet (have to wait cool-down period) + success, children, error = await async_subtensor.wallets.get_children( + hotkey=alice_wallet.hotkey.ss58_address, + block=set_children_block, + netuid=dave_subnet_netuid, + ) + + assert success is True + assert children == [] + assert error == "" + + # children are in pending state + pending, cooldown = await async_subtensor.wallets.get_children_pending( + hotkey=alice_wallet.hotkey.ss58_address, + netuid=dave_subnet_netuid, + ) + + logging.console.info( + f"[orange]block: {await async_subtensor.block}, cooldown: {cooldown}[/orange]" + ) + + assert pending == [(1.0, bob_wallet.hotkey.ss58_address)] + + # we use `*2` to ensure that the chain has time to process + await async_subtensor.wait_for_block(cooldown + SET_CHILDREN_RATE_LIMIT * 2) + + success, children, error = await async_subtensor.wallets.get_children( + hotkey=alice_wallet.hotkey.ss58_address, + netuid=dave_subnet_netuid, + ) + + assert error == "" + assert success is True + assert children == [(1.0, bob_wallet.hotkey.ss58_address)] + + parent_ = await async_subtensor.wallets.get_parents( + bob_wallet.hotkey.ss58_address, dave_subnet_netuid + ) + + assert parent_ == [(1.0, alice_wallet.hotkey.ss58_address)] + + # pending queue is empty + pending, cooldown = await async_subtensor.wallets.get_children_pending( + hotkey=alice_wallet.hotkey.ss58_address, + netuid=dave_subnet_netuid, + ) + assert pending == [] + logging.console.info( + f"[orange]block: {await async_subtensor.block}, cooldown: {cooldown}[/orange]" + ) + + with pytest.raises(TxRateLimitExceeded): + set_children_block = await async_subtensor.chain.get_current_block() + await async_subtensor.extrinsics.set_children( + wallet=alice_wallet, + hotkey=alice_wallet.hotkey.ss58_address, + netuid=dave_subnet_netuid, + children=[], + raise_error=True, + ) + await async_subtensor.extrinsics.set_children( + wallet=alice_wallet, + hotkey=alice_wallet.hotkey.ss58_address, + netuid=dave_subnet_netuid, + children=[], + raise_error=True, + ) + + await async_subtensor.wait_for_block(set_children_block + SET_CHILDREN_RATE_LIMIT) + + success, message = await async_subtensor.extrinsics.set_children( + wallet=alice_wallet, + hotkey=alice_wallet.hotkey.ss58_address, + netuid=dave_subnet_netuid, + children=[], + raise_error=True, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert message == "Success with `set_children_extrinsic` response." + assert success is True + logging.console.info(f"[orange]success: {success}, message: {message}[/orange]") + + set_children_block = await async_subtensor.chain.get_current_block() + + pending, cooldown = await async_subtensor.wallets.get_children_pending( + hotkey=alice_wallet.hotkey.ss58_address, + netuid=dave_subnet_netuid, + ) + + assert pending == [] + logging.console.info( + f"[orange]block: {await async_subtensor.block}, cooldown: {cooldown}[/orange]" + ) + + await async_subtensor.wait_for_block(cooldown) + + success, children, error = await async_subtensor.wallets.get_children( + hotkey=alice_wallet.hotkey.ss58_address, + netuid=dave_subnet_netuid, + ) + + assert error == "" + assert success is True + assert children == [(1.0, bob_wallet.hotkey.ss58_address)] + + await async_subtensor.wait_for_block(set_children_block + SET_CHILDREN_RATE_LIMIT) + + await async_sudo_set_admin_utils( + substrate=async_subtensor.substrate, + wallet=alice_wallet, + call_function="sudo_set_stake_threshold", + call_params={ + "min_stake": 1_000_000_000_000, + }, + ) + + with pytest.raises(NotEnoughStakeToSetChildkeys): + await async_subtensor.extrinsics.set_children( + wallet=alice_wallet, + hotkey=alice_wallet.hotkey.ss58_address, + netuid=dave_subnet_netuid, + children=[ + ( + 1.0, + bob_wallet.hotkey.ss58_address, + ), + ], + raise_error=True, + ) + + logging.console.success(f"✅ Test [green]test_children_async[/green] passed") diff --git a/tests/e2e_tests/test_incentive.py b/tests/e2e_tests/test_incentive.py new file mode 100644 index 0000000000..0cee9beb8f --- /dev/null +++ b/tests/e2e_tests/test_incentive.py @@ -0,0 +1,367 @@ +import asyncio + +import pytest + +from bittensor.utils.btlogging import logging +from tests.e2e_tests.utils.chain_interactions import ( + async_sudo_set_admin_utils, + async_wait_epoch, + sudo_set_admin_utils, + wait_epoch, +) +from tests.e2e_tests.utils.e2e_test_utils import ( + async_wait_to_start_call, + wait_to_start_call, +) + +DURATION_OF_START_CALL = 10 + + +@pytest.mark.asyncio +async def test_incentive(subtensor, templates, alice_wallet, bob_wallet): + """ + Test the incentive mechanism and interaction of miners/validators + + Steps: + 1. Register a subnet as Alice and register Bob + 2. Run Alice as validator & Bob as miner. Wait Epoch + 3. Verify miner has correct: trust, rank, consensus, incentive + 4. Verify validator has correct: validator_permit, validator_trust, dividends, stake + Raises: + AssertionError: If any of the checks or verifications fail + """ + + logging.console.info("Testing [blue]test_incentive[/blue]") + alice_subnet_netuid = subtensor.subnets.get_total_subnets() # 2 + + # Register root as Alice - the subnet owner and validator + assert subtensor.subnets.register_subnet(alice_wallet), "Subnet wasn't created" + + # Verify subnet created successfully + assert subtensor.subnets.subnet_exists(alice_subnet_netuid), ( + "Subnet wasn't created successfully" + ) + + # Disable commit_reveal on the subnet to check proper behavior + status, error = sudo_set_admin_utils( + substrate=subtensor.substrate, + wallet=alice_wallet, + call_function="sudo_set_commit_reveal_weights_enabled", + call_params={ + "netuid": alice_subnet_netuid, + "enabled": False, + }, + ) + assert status is True, error + + assert wait_to_start_call(subtensor, alice_wallet, alice_subnet_netuid) + + # Register Bob as a neuron on the subnet + assert subtensor.subnets.burned_register(bob_wallet, alice_subnet_netuid), ( + "Unable to register Bob as a neuron" + ) + + # Assert two neurons are in network + assert len(subtensor.neurons.neurons(netuid=alice_subnet_netuid)) == 2, ( + "Alice & Bob not registered in the subnet" + ) + + # Wait for the first epoch to pass + await wait_epoch(subtensor, alice_subnet_netuid) + + # Get current miner/validator stats + alice_neuron = subtensor.neurons.neurons(netuid=alice_subnet_netuid)[0] + + assert alice_neuron.validator_permit is True + assert alice_neuron.dividends == 0 + assert alice_neuron.validator_trust == 0 + assert alice_neuron.incentive == 0 + assert alice_neuron.consensus == 0 + assert alice_neuron.rank == 0 + + bob_neuron = subtensor.neurons.neurons(netuid=alice_subnet_netuid)[1] + + assert bob_neuron.incentive == 0 + assert bob_neuron.consensus == 0 + assert bob_neuron.rank == 0 + assert bob_neuron.trust == 0 + + # update weights_set_rate_limit for fast-blocks + tempo = subtensor.subnets.tempo(alice_subnet_netuid) + status, error = sudo_set_admin_utils( + substrate=subtensor.substrate, + wallet=alice_wallet, + call_function="sudo_set_weights_set_rate_limit", + call_params={ + "netuid": alice_subnet_netuid, + "weights_set_rate_limit": tempo, + }, + ) + + assert error is None + assert status is True + + # max attempts to run miner and validator + max_attempt = 3 + while True: + try: + async with templates.miner(bob_wallet, alice_subnet_netuid) as miner: + await asyncio.wait_for(miner.started.wait(), 60) + + async with templates.validator( + alice_wallet, alice_subnet_netuid + ) as validator: + # wait for the Validator to process and set_weights + await asyncio.wait_for(validator.set_weights.wait(), 60) + break + except asyncio.TimeoutError: + if max_attempt > 0: + max_attempt -= 1 + continue + raise + + # wait one tempo (fast block) + next_epoch_start_block = subtensor.subnets.get_next_epoch_start_block( + alice_subnet_netuid + ) + subtensor.wait_for_block(next_epoch_start_block + tempo + 1) + + validators = subtensor.metagraphs.get_metagraph_info( + alice_subnet_netuid, field_indices=[72] + ).validators + + alice_uid = subtensor.subnets.get_uid_for_hotkey_on_subnet( + hotkey_ss58=alice_wallet.hotkey.ss58_address, netuid=alice_subnet_netuid + ) + assert validators[alice_uid] == 1 + + bob_uid = subtensor.subnets.get_uid_for_hotkey_on_subnet( + hotkey_ss58=bob_wallet.hotkey.ss58_address, netuid=alice_subnet_netuid + ) + assert validators[bob_uid] == 0 + + while True: + try: + neurons = subtensor.neurons.neurons(netuid=alice_subnet_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.rank < 0.5 + + bob_neuron = neurons[1] + + assert bob_neuron.incentive > 0.5 + assert bob_neuron.consensus > 0.5 + assert bob_neuron.rank > 0.5 + assert bob_neuron.trust == 1 + + bonds = subtensor.subnets.bonds(alice_subnet_netuid) + + assert bonds == [ + ( + 0, + [ + (0, 65535), + (1, 65535), + ], + ), + ( + 1, + [], + ), + ] + + print("✅ Passed test_incentive") + break + except Exception: + subtensor.wait_for_block(subtensor.block) + continue + + logging.console.success("Test [green]test_incentive[/green] passed.") + + +@pytest.mark.asyncio +async def test_incentive_async(async_subtensor, templates, alice_wallet, bob_wallet): + """ + Test the incentive mechanism and interaction of miners/validators + + Steps: + 1. Register a subnet as Alice and register Bob + 2. Run Alice as validator & Bob as miner. Wait Epoch + 3. Verify miner has correct: trust, rank, consensus, incentive + 4. Verify validator has correct: validator_permit, validator_trust, dividends, stake + Raises: + AssertionError: If any of the checks or verifications fail + """ + + logging.console.info("Testing [blue]test_incentive[/blue]") + alice_subnet_netuid = await async_subtensor.subnets.get_total_subnets() # 2 + + # Register root as Alice - the subnet owner and validator + assert await async_subtensor.subnets.register_subnet(alice_wallet), ( + "Subnet wasn't created" + ) + + # Verify subnet created successfully + assert await async_subtensor.subnets.subnet_exists(alice_subnet_netuid), ( + "Subnet wasn't created successfully" + ) + + # Disable commit_reveal on the subnet to check proper behavior + status, error = await async_sudo_set_admin_utils( + substrate=async_subtensor.substrate, + wallet=alice_wallet, + call_function="sudo_set_commit_reveal_weights_enabled", + call_params={ + "netuid": alice_subnet_netuid, + "enabled": False, + }, + ) + assert status is True, error + + assert await async_wait_to_start_call( + async_subtensor, alice_wallet, alice_subnet_netuid + ) + + # Register Bob as a neuron on the subnet + assert await async_subtensor.subnets.burned_register( + bob_wallet, alice_subnet_netuid + ), "Unable to register Bob as a neuron" + + # Assert two neurons are in network + assert ( + len(await async_subtensor.neurons.neurons(netuid=alice_subnet_netuid)) == 2 + ), "Alice & Bob not registered in the subnet" + + # Wait for the first epoch to pass + await async_wait_epoch(async_subtensor, alice_subnet_netuid) + + # Get current miner/validator stats + alice_neuron = (await async_subtensor.neurons.neurons(netuid=alice_subnet_netuid))[ + 0 + ] + + assert alice_neuron.validator_permit is True + assert alice_neuron.dividends == 0 + assert alice_neuron.validator_trust == 0 + assert alice_neuron.incentive == 0 + assert alice_neuron.consensus == 0 + assert alice_neuron.rank == 0 + + bob_neuron = (await async_subtensor.neurons.neurons(netuid=alice_subnet_netuid))[1] + + assert bob_neuron.incentive == 0 + assert bob_neuron.consensus == 0 + assert bob_neuron.rank == 0 + assert bob_neuron.trust == 0 + + # update weights_set_rate_limit for fast-blocks + tempo = await async_subtensor.subnets.tempo(alice_subnet_netuid) + status, error = await async_sudo_set_admin_utils( + substrate=async_subtensor.substrate, + wallet=alice_wallet, + call_function="sudo_set_weights_set_rate_limit", + call_params={ + "netuid": alice_subnet_netuid, + "weights_set_rate_limit": tempo, + }, + ) + + assert error is None + assert status is True + + # max attempts to run miner and validator + max_attempt = 3 + while True: + try: + async with templates.miner(bob_wallet, alice_subnet_netuid) as miner: + await asyncio.wait_for(miner.started.wait(), 60) + + async with templates.validator( + alice_wallet, alice_subnet_netuid + ) as validator: + # wait for the Validator to process and set_weights + await asyncio.wait_for(validator.set_weights.wait(), 60) + break + except asyncio.TimeoutError: + if max_attempt > 0: + max_attempt -= 1 + continue + raise + + # wait one tempo (fast block) + next_epoch_start_block = await async_subtensor.subnets.get_next_epoch_start_block( + alice_subnet_netuid + ) + await async_subtensor.wait_for_block(next_epoch_start_block + tempo + 1) + + validators = ( + await async_subtensor.metagraphs.get_metagraph_info( + alice_subnet_netuid, field_indices=[72] + ) + ).validators + + alice_uid = await async_subtensor.subnets.get_uid_for_hotkey_on_subnet( + hotkey_ss58=alice_wallet.hotkey.ss58_address, netuid=alice_subnet_netuid + ) + assert validators[alice_uid] == 1 + + bob_uid = await async_subtensor.subnets.get_uid_for_hotkey_on_subnet( + hotkey_ss58=bob_wallet.hotkey.ss58_address, netuid=alice_subnet_netuid + ) + assert validators[bob_uid] == 0 + + while True: + try: + neurons = await async_subtensor.neurons.neurons(netuid=alice_subnet_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.rank < 0.5 + + bob_neuron = neurons[1] + + assert bob_neuron.incentive > 0.5 + assert bob_neuron.consensus > 0.5 + assert bob_neuron.rank > 0.5 + assert bob_neuron.trust == 1 + + bonds = await async_subtensor.subnets.bonds(alice_subnet_netuid) + + assert bonds == [ + ( + 0, + [ + (0, 65535), + (1, 65535), + ], + ), + ( + 1, + [], + ), + ] + + print("✅ Passed test_incentive") + break + except Exception: + await async_subtensor.wait_for_block(await async_subtensor.block) + continue + + logging.console.success("Test [green]test_incentive[/green] passed.") diff --git a/tests/e2e_tests/test_liquid_alpha.py b/tests/e2e_tests/test_liquid_alpha.py new file mode 100644 index 0000000000..e31a852933 --- /dev/null +++ b/tests/e2e_tests/test_liquid_alpha.py @@ -0,0 +1,382 @@ +import pytest + +from bittensor.utils.balance import Balance +from bittensor.utils.btlogging import logging +from tests.e2e_tests.utils.chain_interactions import ( + async_sudo_set_hyperparameter_bool, + async_sudo_set_hyperparameter_values, + sudo_set_hyperparameter_bool, + sudo_set_hyperparameter_values, +) +from tests.e2e_tests.utils.e2e_test_utils import ( + async_wait_to_start_call, + wait_to_start_call, +) + + +def liquid_alpha_call_params(netuid: int, alpha_values: str): + alpha_low, alpha_high = [v.strip() for v in alpha_values.split(",")] + return { + "netuid": netuid, + "alpha_low": alpha_low, + "alpha_high": alpha_high, + } + + +def test_liquid_alpha(subtensor, alice_wallet): + """ + Test the liquid alpha mechanism. By June 17 2025 the limits are `0.025 <= alpha_low <= alpha_high <= 1` + + Steps: + 1. Register a subnet through Alice + 2. Register Alice's neuron and add stake + 3. Verify we can't set alpha values without enabling liquid_alpha + 4. Test setting alpha values after enabling liquid_alpha + 5. Verify failures when setting incorrect values (upper and lower bounds) + Raises: + AssertionError: If any of the checks or verifications fail + """ + logging.console.info("Testing [blue]test_liquid_alpha[/blue]") + + u16_max = 65535 + netuid = 2 + + # Register root as Alice + assert subtensor.subnets.register_subnet(alice_wallet), ( + "Unable to register the subnet" + ) + + # Verify subnet created successfully + assert subtensor.subnets.subnet_exists(netuid) + + assert wait_to_start_call(subtensor, alice_wallet, netuid) + + # Register a neuron (Alice) to the subnet + assert subtensor.subnets.burned_register(alice_wallet, netuid), ( + "Unable to register Alice as a neuron" + ) + + # Stake to become to top neuron after the first epoch + assert subtensor.staking.add_stake( + wallet=alice_wallet, + netuid=netuid, + hotkey_ss58=alice_wallet.hotkey.ss58_address, + amount=Balance.from_tao(10_000), + ), "Unable to stake to Alice neuron" + + # Assert liquid alpha is disabled + assert ( + subtensor.subnets.get_subnet_hyperparameters(netuid=netuid).liquid_alpha_enabled + is False + ), "Liquid alpha is enabled by default" + + # Attempt to set alpha high/low while disabled (should fail) + alpha_values = "6553, 53083" + call_params = liquid_alpha_call_params(netuid, alpha_values) + result, error_message = sudo_set_hyperparameter_values( + substrate=subtensor.substrate, + wallet=alice_wallet, + call_function="sudo_set_alpha_values", + call_params=call_params, + return_error_message=True, + ) + assert result is False, "Alpha values set while being disabled" + assert error_message["name"] == "LiquidAlphaDisabled" + + # Enabled liquid alpha on the subnet + assert sudo_set_hyperparameter_bool( + substrate=subtensor.substrate, + wallet=alice_wallet, + call_function="sudo_set_liquid_alpha_enabled", + value=True, + netuid=netuid, + ), "Unable to enable liquid alpha" + + assert subtensor.subnets.get_subnet_hyperparameters( + netuid, + ).liquid_alpha_enabled, "Failed to enable liquid alpha" + + # Attempt to set alpha high & low after enabling the hyperparameter + alpha_values = "26001, 54099" + call_params = liquid_alpha_call_params(netuid, alpha_values) + assert sudo_set_hyperparameter_values( + substrate=subtensor.substrate, + wallet=alice_wallet, + call_function="sudo_set_alpha_values", + call_params=call_params, + ), "Unable to set alpha_values" + assert subtensor.subnets.get_subnet_hyperparameters(netuid).alpha_high == 54099, ( + "Failed to set alpha high" + ) + assert subtensor.subnets.get_subnet_hyperparameters(netuid).alpha_low == 26001, ( + "Failed to set alpha low" + ) + + # Testing alpha high upper and lower bounds + + # 1. Test setting Alpha_high too low + alpha_high_too_low = 87 + + call_params = liquid_alpha_call_params(netuid, f"6553, {alpha_high_too_low}") + result, error_message = sudo_set_hyperparameter_values( + substrate=subtensor.substrate, + wallet=alice_wallet, + call_function="sudo_set_alpha_values", + call_params=call_params, + return_error_message=True, + ) + + assert result is False, "Able to set incorrect alpha_high value" + assert error_message["name"] == "AlphaHighTooLow" + + # 2. Test setting Alpha_high too high + alpha_high_too_high = u16_max + 1 # One more than the max acceptable value + call_params = liquid_alpha_call_params(netuid, f"6553, {alpha_high_too_high}") + try: + sudo_set_hyperparameter_values( + substrate=subtensor.substrate, + wallet=alice_wallet, + call_function="sudo_set_alpha_values", + call_params=call_params, + return_error_message=True, + ) + except Exception as e: + assert str(e) == "65536 out of range for u16", f"Unexpected error: {e}" + + # Testing alpha low upper and lower bounds + + # 1. Test setting Alpha_low too low + alpha_low_too_low = 0 + call_params = liquid_alpha_call_params(netuid, f"{alpha_low_too_low}, 53083") + result, error_message = sudo_set_hyperparameter_values( + substrate=subtensor.substrate, + wallet=alice_wallet, + call_function="sudo_set_alpha_values", + call_params=call_params, + return_error_message=True, + ) + assert result is False, "Able to set incorrect alpha_low value" + assert error_message["name"] == "AlphaLowOutOfRange" + + # 2. Test setting Alpha_low too high + alpha_low_too_high = 53084 + call_params = liquid_alpha_call_params(netuid, f"{alpha_low_too_high}, 53083") + result, error_message = sudo_set_hyperparameter_values( + substrate=subtensor.substrate, + wallet=alice_wallet, + call_function="sudo_set_alpha_values", + call_params=call_params, + return_error_message=True, + ) + assert result is False, "Able to set incorrect alpha_low value" + assert error_message["name"] == "AlphaLowOutOfRange" + + # Setting normal alpha values + alpha_values = "6553, 53083" + call_params = liquid_alpha_call_params(netuid, alpha_values) + assert sudo_set_hyperparameter_values( + substrate=subtensor.substrate, + wallet=alice_wallet, + call_function="sudo_set_alpha_values", + call_params=call_params, + ), "Unable to set liquid alpha values" + + assert subtensor.subnets.get_subnet_hyperparameters(netuid).alpha_high == 53083, ( + "Failed to set alpha high" + ) + assert subtensor.subnets.get_subnet_hyperparameters(netuid).alpha_low == 6553, ( + "Failed to set alpha low" + ) + + # Disable Liquid Alpha + assert sudo_set_hyperparameter_bool( + substrate=subtensor.substrate, + wallet=alice_wallet, + call_function="sudo_set_liquid_alpha_enabled", + value=False, + netuid=netuid, + ), "Unable to disable liquid alpha" + + assert ( + subtensor.subnets.get_subnet_hyperparameters(netuid).liquid_alpha_enabled + is False + ), "Failed to disable liquid alpha" + logging.console.info("✅ Passed [blue]test_liquid_alpha[/blue]") + + +@pytest.mark.asyncio +async def test_liquid_alpha_async(async_subtensor, alice_wallet): + """ + Async test the liquid alpha mechanism. By June 17 2025 the limits are `0.025 <= alpha_low <= alpha_high <= 1` + + Steps: + 1. Register a subnet through Alice + 2. Register Alice's neuron and add stake + 3. Verify we can't set alpha values without enabling liquid_alpha + 4. Test setting alpha values after enabling liquid_alpha + 5. Verify failures when setting incorrect values (upper and lower bounds) + Raises: + AssertionError: If any of the checks or verifications fail + """ + logging.console.info("Testing [blue]test_liquid_alpha_async[/blue]") + + u16_max = 65535 + netuid = 2 + + # Register root as Alice + assert await async_subtensor.subnets.register_subnet(alice_wallet), ( + "Unable to register the subnet" + ) + + # Verify subnet created successfully + assert await async_subtensor.subnets.subnet_exists(netuid) + + assert await async_wait_to_start_call(async_subtensor, alice_wallet, netuid) + + # Register a neuron (Alice) to the subnet + assert await async_subtensor.subnets.burned_register(alice_wallet, netuid), ( + "Unable to register Alice as a neuron" + ) + + # Stake to become to top neuron after the first epoch + assert await async_subtensor.staking.add_stake( + wallet=alice_wallet, + netuid=netuid, + hotkey_ss58=alice_wallet.hotkey.ss58_address, + amount=Balance.from_tao(10_000), + ), "Unable to stake to Alice neuron" + + # Assert liquid alpha is disabled + assert ( + await async_subtensor.subnets.get_subnet_hyperparameters(netuid=netuid) + ).liquid_alpha_enabled is False, "Liquid alpha is enabled by default" + + # Attempt to set alpha high/low while disabled (should fail) + alpha_values = "6553, 53083" + call_params = liquid_alpha_call_params(netuid, alpha_values) + result, error_message = await async_sudo_set_hyperparameter_values( + substrate=async_subtensor.substrate, + wallet=alice_wallet, + call_function="sudo_set_alpha_values", + call_params=call_params, + return_error_message=True, + ) + assert result is False, "Alpha values set while being disabled" + assert error_message["name"] == "LiquidAlphaDisabled" + + # Enabled liquid alpha on the subnet + assert await async_sudo_set_hyperparameter_bool( + substrate=async_subtensor.substrate, + wallet=alice_wallet, + call_function="sudo_set_liquid_alpha_enabled", + value=True, + netuid=netuid, + ), "Unable to enable liquid alpha" + + assert ( + await async_subtensor.subnets.get_subnet_hyperparameters( + netuid, + ) + ).liquid_alpha_enabled, "Failed to enable liquid alpha" + + # Attempt to set alpha high & low after enabling the hyperparameter + alpha_values = "26001, 54099" + call_params = liquid_alpha_call_params(netuid, alpha_values) + assert await async_sudo_set_hyperparameter_values( + substrate=async_subtensor.substrate, + wallet=alice_wallet, + call_function="sudo_set_alpha_values", + call_params=call_params, + ), "Unable to set alpha_values" + + sn_hps = await async_subtensor.subnets.get_subnet_hyperparameters(netuid) + assert sn_hps.alpha_high == 54099, "Failed to set alpha high" + assert sn_hps.alpha_low == 26001, "Failed to set alpha low" + + # Testing alpha high upper and lower bounds + + # 1. Test setting Alpha_high too low + alpha_high_too_low = 87 + + call_params = liquid_alpha_call_params(netuid, f"6553, {alpha_high_too_low}") + result, error_message = await async_sudo_set_hyperparameter_values( + substrate=async_subtensor.substrate, + wallet=alice_wallet, + call_function="sudo_set_alpha_values", + call_params=call_params, + return_error_message=True, + ) + + assert result is False, "Able to set incorrect alpha_high value" + assert error_message["name"] == "AlphaHighTooLow" + + # 2. Test setting Alpha_high too high + alpha_high_too_high = u16_max + 1 # One more than the max acceptable value + call_params = liquid_alpha_call_params(netuid, f"6553, {alpha_high_too_high}") + try: + await async_sudo_set_hyperparameter_values( + substrate=async_subtensor.substrate, + wallet=alice_wallet, + call_function="sudo_set_alpha_values", + call_params=call_params, + return_error_message=True, + ) + except Exception as e: + assert str(e) == "65536 out of range for u16", f"Unexpected error: {e}" + + # Testing alpha low upper and lower bounds + + # 1. Test setting Alpha_low too low + alpha_low_too_low = 0 + call_params = liquid_alpha_call_params(netuid, f"{alpha_low_too_low}, 53083") + result, error_message = await async_sudo_set_hyperparameter_values( + substrate=async_subtensor.substrate, + wallet=alice_wallet, + call_function="sudo_set_alpha_values", + call_params=call_params, + return_error_message=True, + ) + assert result is False, "Able to set incorrect alpha_low value" + assert error_message["name"] == "AlphaLowOutOfRange" + + # 2. Test setting Alpha_low too high + alpha_low_too_high = 53084 + call_params = liquid_alpha_call_params(netuid, f"{alpha_low_too_high}, 53083") + result, error_message = await async_sudo_set_hyperparameter_values( + substrate=async_subtensor.substrate, + wallet=alice_wallet, + call_function="sudo_set_alpha_values", + call_params=call_params, + return_error_message=True, + ) + assert result is False, "Able to set incorrect alpha_low value" + assert error_message["name"] == "AlphaLowOutOfRange" + + # Setting normal alpha values + alpha_values = "6553, 53083" + call_params = liquid_alpha_call_params(netuid, alpha_values) + assert await async_sudo_set_hyperparameter_values( + substrate=async_subtensor.substrate, + wallet=alice_wallet, + call_function="sudo_set_alpha_values", + call_params=call_params, + ), "Unable to set liquid alpha values" + + sn_hps = await async_subtensor.subnets.get_subnet_hyperparameters(netuid) + assert sn_hps.alpha_high == 53083, "Failed to set alpha high" + assert sn_hps.alpha_low == 6553, "Failed to set alpha low" + + # Disable Liquid Alpha + assert await async_sudo_set_hyperparameter_bool( + substrate=async_subtensor.substrate, + wallet=alice_wallet, + call_function="sudo_set_liquid_alpha_enabled", + value=False, + netuid=netuid, + ), "Unable to disable liquid alpha" + + assert ( + await async_subtensor.subnets.get_subnet_hyperparameters(netuid) + ).liquid_alpha_enabled is False, "Failed to disable liquid alpha" + + logging.console.info("✅ Passed [blue]test_liquid_alpha_async[/blue]") diff --git a/tests/e2e_tests/test_liquidity.py b/tests/e2e_tests/test_liquidity.py new file mode 100644 index 0000000000..2617a6259c --- /dev/null +++ b/tests/e2e_tests/test_liquidity.py @@ -0,0 +1,594 @@ +import pytest + +from bittensor import Balance, logging +from tests.e2e_tests.utils.e2e_test_utils import ( + async_wait_to_start_call, + wait_to_start_call, +) +from bittensor.utils.liquidity import LiquidityPosition + + +@pytest.mark.asyncio +async def test_liquidity(subtensor, alice_wallet, bob_wallet): + """ + Tests the liquidity mechanism + + Steps: + 1. Check `get_liquidity_list` return None if SN doesn't exist. + 2. Register a subnet through Alice. + 3. Make sure `get_liquidity_list` return None without activ SN. + 4. Wait until start call availability and do this call. + 5. Add liquidity to the subnet and check `get_liquidity_list` return liquidity positions. + 6. Modify liquidity and check `get_liquidity_list` return new liquidity positions with modified liquidity value. + 7. Add second liquidity position and check `get_liquidity_list` return new liquidity positions with 0 index. + 8. Add stake from Bob to Alice and check `get_liquidity_list` return new liquidity positions with fees_tao. + 9. Remove all stake from Alice and check `get_liquidity_list` return new liquidity positions with 0 fees_tao. + 10. Remove all liquidity positions and check `get_liquidity_list` return empty list. + """ + logging.console.info("Testing [blue]test_liquidity[/blue]") + + alice_subnet_netuid = subtensor.subnets.get_total_subnets() # 2 + + # Make sure `get_liquidity_list` return None if SN doesn't exist + assert ( + subtensor.subnets.get_liquidity_list( + wallet=alice_wallet, netuid=alice_subnet_netuid + ) + is None + ), "❌ `get_liquidity_list` is not None for unexisting subnet." + + # Register root as Alice + assert subtensor.subnets.register_subnet(alice_wallet), ( + "❌ Unable to register the subnet" + ) + + # Verify subnet 2 created successfully + assert subtensor.subnets.subnet_exists(alice_subnet_netuid), ( + f"❌ Subnet {alice_subnet_netuid} wasn't created successfully" + ) + + # Make sure `get_liquidity_list` return None without activ SN + assert ( + subtensor.subnets.get_liquidity_list( + wallet=alice_wallet, netuid=alice_subnet_netuid + ) + is None + ), "❌ `get_liquidity_list` is not None when no activ subnet." + + # Wait until start call availability and do this call + assert wait_to_start_call(subtensor, alice_wallet, alice_subnet_netuid) + + # Make sure `get_liquidity_list` return None without activ SN + assert ( + subtensor.subnets.get_liquidity_list( + wallet=alice_wallet, netuid=alice_subnet_netuid + ) + == [] + ), "❌ `get_liquidity_list` is not empty list before fist liquidity add." + + # enable user liquidity in SN + success, message = subtensor.extrinsics.toggle_user_liquidity( + wallet=alice_wallet, + netuid=alice_subnet_netuid, + enable=True, + ) + assert success, message + assert message == "", "❌ Cannot enable user liquidity." + + # Add steak to call add_liquidity + assert subtensor.staking.add_stake( + wallet=alice_wallet, + hotkey_ss58=alice_wallet.hotkey.ss58_address, + netuid=alice_subnet_netuid, + amount=Balance.from_tao(1), + ), "❌ Cannot cannot add stake to Alice from Alice." + + # wait for the next block to give the chain time to update the stake + subtensor.wait_for_block() + + current_balance = subtensor.wallets.get_balance(alice_wallet.hotkey.ss58_address) + current_sn_stake = subtensor.staking.get_stake_info_for_coldkey( + coldkey_ss58=alice_wallet.coldkey.ss58_address + ) + logging.console.info( + f"Alice balance: {current_balance} and stake: {current_sn_stake}" + ) + + # Add liquidity + success, message = subtensor.extrinsics.add_liquidity( + wallet=alice_wallet, + netuid=alice_subnet_netuid, + liquidity=Balance.from_tao(1), + price_low=Balance.from_tao(1.7), + price_high=Balance.from_tao(1.8), + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert success, message + assert message == "", "❌ Cannot add liquidity." + + # Get liquidity + liquidity_positions = subtensor.subnets.get_liquidity_list( + wallet=alice_wallet, netuid=alice_subnet_netuid + ) + + assert len(liquidity_positions) == 1, ( + "❌ liquidity_positions has more than one element." + ) + + # Check if liquidity is correct + liquidity_position = liquidity_positions[0] + assert liquidity_position == LiquidityPosition( + id=2, + price_low=liquidity_position.price_low, + price_high=liquidity_position.price_high, + liquidity=Balance.from_tao(1), + fees_tao=Balance.from_tao(0), + fees_alpha=Balance.from_tao(0, netuid=alice_subnet_netuid), + netuid=alice_subnet_netuid, + ), "❌ `get_liquidity_list` still empty list after liquidity add." + + # Modify liquidity position with positive value + success, message = subtensor.extrinsics.modify_liquidity( + wallet=alice_wallet, + netuid=alice_subnet_netuid, + position_id=liquidity_position.id, + liquidity_delta=Balance.from_tao(20), + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert success, message + assert message == "", "❌ cannot modify liquidity position." + + liquidity_positions = subtensor.subnets.get_liquidity_list( + wallet=alice_wallet, netuid=alice_subnet_netuid + ) + + assert len(liquidity_positions) == 1, ( + "❌ liquidity_positions has more than one element." + ) + liquidity_position = liquidity_positions[0] + + assert liquidity_position == LiquidityPosition( + id=2, + price_low=liquidity_position.price_low, + price_high=liquidity_position.price_high, + liquidity=Balance.from_tao(21), + fees_tao=Balance.from_tao(0), + fees_alpha=Balance.from_tao(0, netuid=alice_subnet_netuid), + netuid=alice_subnet_netuid, + ) + + # Modify liquidity position with negative value + success, message = subtensor.extrinsics.modify_liquidity( + wallet=alice_wallet, + netuid=alice_subnet_netuid, + position_id=liquidity_position.id, + liquidity_delta=-Balance.from_tao(11), + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert success, message + assert message == "", "❌ cannot modify liquidity position." + + liquidity_positions = subtensor.subnets.get_liquidity_list( + wallet=alice_wallet, netuid=alice_subnet_netuid + ) + + assert len(liquidity_positions) == 1, ( + "❌ liquidity_positions has more than one element." + ) + liquidity_position = liquidity_positions[0] + + assert liquidity_position == LiquidityPosition( + id=2, + price_low=liquidity_position.price_low, + price_high=liquidity_position.price_high, + liquidity=Balance.from_tao(10), + fees_tao=Balance.from_tao(0), + fees_alpha=Balance.from_tao(0, netuid=alice_subnet_netuid), + netuid=alice_subnet_netuid, + ) + + # Add stake from Bob to Alice + assert subtensor.staking.add_stake( + wallet=bob_wallet, + hotkey_ss58=alice_wallet.hotkey.ss58_address, + netuid=alice_subnet_netuid, + amount=Balance.from_tao(1000), + ), "❌ Cannot add stake from Bob to Alice." + + # wait for the next block to give the chain time to update the stake + subtensor.wait_for_block() + + current_balance = subtensor.wallets.get_balance(alice_wallet.hotkey.ss58_address) + current_sn_stake = subtensor.staking.get_stake_info_for_coldkey( + coldkey_ss58=alice_wallet.coldkey.ss58_address + ) + logging.console.info( + f"Alice balance: {current_balance} and stake: {current_sn_stake}" + ) + + # Add second liquidity position + success, message = subtensor.extrinsics.add_liquidity( + wallet=alice_wallet, + netuid=alice_subnet_netuid, + liquidity=Balance.from_tao(150), + price_low=Balance.from_tao(0.8), + price_high=Balance.from_tao(1.2), + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert success, message + assert message == "", "❌ Cannot add liquidity." + + liquidity_positions = subtensor.subnets.get_liquidity_list( + wallet=alice_wallet, netuid=alice_subnet_netuid + ) + + assert len(liquidity_positions) == 2, ( + f"❌ liquidity_positions should have 2 elements, but has only {len(liquidity_positions)} element." + ) + + # All new liquidity inserts on the 0 index + liquidity_position_second = liquidity_positions[0] + assert liquidity_position_second == LiquidityPosition( + id=3, + price_low=liquidity_position_second.price_low, + price_high=liquidity_position_second.price_high, + liquidity=Balance.from_tao(150), + fees_tao=Balance.from_tao(0), + fees_alpha=Balance.from_tao(0, netuid=alice_subnet_netuid), + netuid=alice_subnet_netuid, + ) + + liquidity_position_first = liquidity_positions[1] + assert liquidity_position_first == LiquidityPosition( + id=2, + price_low=liquidity_position_first.price_low, + price_high=liquidity_position_first.price_high, + liquidity=Balance.from_tao(10), + fees_tao=liquidity_position_first.fees_tao, + fees_alpha=Balance.from_tao(0, netuid=alice_subnet_netuid), + netuid=alice_subnet_netuid, + ) + # After adding stake alice liquidity position has a fees_tao bc of high price + assert liquidity_position_first.fees_tao > Balance.from_tao(0) + + # Bob remove all stake from alice + assert subtensor.extrinsics.unstake_all( + wallet=bob_wallet, + hotkey=alice_wallet.hotkey.ss58_address, + netuid=alice_subnet_netuid, + rate_tolerance=0.9, # keep high rate tolerance to avoid flaky behavior + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + # Check that fees_alpha comes too after all unstake + liquidity_position_first = subtensor.subnets.get_liquidity_list( + wallet=alice_wallet, netuid=alice_subnet_netuid + )[1] + assert liquidity_position_first.fees_tao > Balance.from_tao(0) + assert liquidity_position_first.fees_alpha > Balance.from_tao( + 0, alice_subnet_netuid + ) + + # Remove all liquidity positions + for p in liquidity_positions: + success, message = subtensor.extrinsics.remove_liquidity( + wallet=alice_wallet, + netuid=alice_subnet_netuid, + position_id=p.id, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert success, message + assert message == "", "❌ Cannot remove liquidity." + + # Make sure all liquidity positions removed + assert ( + subtensor.subnets.get_liquidity_list( + wallet=alice_wallet, netuid=alice_subnet_netuid + ) + == [] + ), "❌ Not all liquidity positions removed." + + logging.console.info("✅ Passed [blue]test_liquidity[/blue]") + + +@pytest.mark.asyncio +async def test_liquidity_async(async_subtensor, alice_wallet, bob_wallet): + """ + ASync tests the liquidity mechanism + + Steps: + 1. Check `get_liquidity_list` return None if SN doesn't exist. + 2. Register a subnet through Alice. + 3. Make sure `get_liquidity_list` return None without activ SN. + 4. Wait until start call availability and do this call. + 5. Add liquidity to the subnet and check `get_liquidity_list` return liquidity positions. + 6. Modify liquidity and check `get_liquidity_list` return new liquidity positions with modified liquidity value. + 7. Add second liquidity position and check `get_liquidity_list` return new liquidity positions with 0 index. + 8. Add stake from Bob to Alice and check `get_liquidity_list` return new liquidity positions with fees_tao. + 9. Remove all stake from Alice and check `get_liquidity_list` return new liquidity positions with 0 fees_tao. + 10. Remove all liquidity positions and check `get_liquidity_list` return empty list. + """ + logging.console.info("Testing [blue]test_liquidity_async[/blue]") + + alice_subnet_netuid = await async_subtensor.subnets.get_total_subnets() # 2 + + # Make sure `get_liquidity_list` return None if SN doesn't exist + assert ( + await async_subtensor.subnets.get_liquidity_list( + wallet=alice_wallet, netuid=alice_subnet_netuid + ) + is None + ), "❌ `get_liquidity_list` is not None for unexisting subnet." + + # Register root as Alice + assert await async_subtensor.subnets.register_subnet(alice_wallet), ( + "❌ Unable to register the subnet" + ) + + # Verify subnet 2 created successfully + assert await async_subtensor.subnets.subnet_exists(alice_subnet_netuid), ( + f"❌ Subnet {alice_subnet_netuid} wasn't created successfully" + ) + + # Make sure `get_liquidity_list` return None without activ SN + assert ( + await async_subtensor.subnets.get_liquidity_list( + wallet=alice_wallet, netuid=alice_subnet_netuid + ) + is None + ), "❌ `get_liquidity_list` is not None when no activ subnet." + + # Wait until start call availability and do this call + assert await async_wait_to_start_call( + async_subtensor, alice_wallet, alice_subnet_netuid + ) + + # Make sure `get_liquidity_list` return None without activ SN + assert ( + await async_subtensor.subnets.get_liquidity_list( + wallet=alice_wallet, netuid=alice_subnet_netuid + ) + == [] + ), "❌ `get_liquidity_list` is not empty list before fist liquidity add." + + # enable user liquidity in SN + success, message = await async_subtensor.extrinsics.toggle_user_liquidity( + wallet=alice_wallet, + netuid=alice_subnet_netuid, + enable=True, + ) + assert success, message + assert message == "", "❌ Cannot enable user liquidity." + + # Add steak to call add_liquidity + assert await async_subtensor.staking.add_stake( + wallet=alice_wallet, + hotkey_ss58=alice_wallet.hotkey.ss58_address, + netuid=alice_subnet_netuid, + amount=Balance.from_tao(1), + ), "❌ Cannot cannot add stake to Alice from Alice." + + # wait for the next block to give the chain time to update the stake + await async_subtensor.wait_for_block() + + current_balance = await async_subtensor.wallets.get_balance( + alice_wallet.hotkey.ss58_address + ) + current_sn_stake = await async_subtensor.staking.get_stake_info_for_coldkey( + coldkey_ss58=alice_wallet.coldkey.ss58_address + ) + logging.console.info( + f"Alice balance: {current_balance} and stake: {current_sn_stake}" + ) + + # Add liquidity + success, message = await async_subtensor.extrinsics.add_liquidity( + wallet=alice_wallet, + netuid=alice_subnet_netuid, + liquidity=Balance.from_tao(1), + price_low=Balance.from_tao(1.7), + price_high=Balance.from_tao(1.8), + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert success, message + assert message == "", "❌ Cannot add liquidity." + + # Get liquidity + liquidity_positions = await async_subtensor.subnets.get_liquidity_list( + wallet=alice_wallet, netuid=alice_subnet_netuid + ) + + assert len(liquidity_positions) == 1, ( + "❌ liquidity_positions has more than one element." + ) + + # Check if liquidity is correct + liquidity_position = liquidity_positions[0] + assert liquidity_position == LiquidityPosition( + id=2, + price_low=liquidity_position.price_low, + price_high=liquidity_position.price_high, + liquidity=Balance.from_tao(1), + fees_tao=Balance.from_tao(0), + fees_alpha=Balance.from_tao(0, netuid=alice_subnet_netuid), + netuid=alice_subnet_netuid, + ), "❌ `get_liquidity_list` still empty list after liquidity add." + + # Modify liquidity position with positive value + success, message = await async_subtensor.extrinsics.modify_liquidity( + wallet=alice_wallet, + netuid=alice_subnet_netuid, + position_id=liquidity_position.id, + liquidity_delta=Balance.from_tao(20), + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert success, message + assert message == "", "❌ cannot modify liquidity position." + + liquidity_positions = await async_subtensor.subnets.get_liquidity_list( + wallet=alice_wallet, netuid=alice_subnet_netuid + ) + + assert len(liquidity_positions) == 1, ( + "❌ liquidity_positions has more than one element." + ) + liquidity_position = liquidity_positions[0] + + assert liquidity_position == LiquidityPosition( + id=2, + price_low=liquidity_position.price_low, + price_high=liquidity_position.price_high, + liquidity=Balance.from_tao(21), + fees_tao=Balance.from_tao(0), + fees_alpha=Balance.from_tao(0, netuid=alice_subnet_netuid), + netuid=alice_subnet_netuid, + ) + + # Modify liquidity position with negative value + success, message = await async_subtensor.extrinsics.modify_liquidity( + wallet=alice_wallet, + netuid=alice_subnet_netuid, + position_id=liquidity_position.id, + liquidity_delta=-Balance.from_tao(11), + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert success, message + assert message == "", "❌ cannot modify liquidity position." + + liquidity_positions = await async_subtensor.subnets.get_liquidity_list( + wallet=alice_wallet, netuid=alice_subnet_netuid + ) + + assert len(liquidity_positions) == 1, ( + "❌ liquidity_positions has more than one element." + ) + liquidity_position = liquidity_positions[0] + + assert liquidity_position == LiquidityPosition( + id=2, + price_low=liquidity_position.price_low, + price_high=liquidity_position.price_high, + liquidity=Balance.from_tao(10), + fees_tao=Balance.from_tao(0), + fees_alpha=Balance.from_tao(0, netuid=alice_subnet_netuid), + netuid=alice_subnet_netuid, + ) + + # Add stake from Bob to Alice + assert await async_subtensor.staking.add_stake( + wallet=bob_wallet, + hotkey_ss58=alice_wallet.hotkey.ss58_address, + netuid=alice_subnet_netuid, + amount=Balance.from_tao(1000), + ), "❌ Cannot add stake from Bob to Alice." + + # wait for the next block to give the chain time to update the stake + await async_subtensor.wait_for_block() + + current_balance = await async_subtensor.wallets.get_balance( + alice_wallet.hotkey.ss58_address + ) + current_sn_stake = await async_subtensor.staking.get_stake_info_for_coldkey( + coldkey_ss58=alice_wallet.coldkey.ss58_address + ) + logging.console.info( + f"Alice balance: {current_balance} and stake: {current_sn_stake}" + ) + + # Add second liquidity position + success, message = await async_subtensor.extrinsics.add_liquidity( + wallet=alice_wallet, + netuid=alice_subnet_netuid, + liquidity=Balance.from_tao(150), + price_low=Balance.from_tao(0.8), + price_high=Balance.from_tao(1.2), + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert success, message + assert message == "", "❌ Cannot add liquidity." + + liquidity_positions = await async_subtensor.subnets.get_liquidity_list( + wallet=alice_wallet, netuid=alice_subnet_netuid + ) + + assert len(liquidity_positions) == 2, ( + f"❌ liquidity_positions should have 2 elements, but has only {len(liquidity_positions)} element." + ) + + # All new liquidity inserts on the 0 index + liquidity_position_second = liquidity_positions[0] + assert liquidity_position_second == LiquidityPosition( + id=3, + price_low=liquidity_position_second.price_low, + price_high=liquidity_position_second.price_high, + liquidity=Balance.from_tao(150), + fees_tao=Balance.from_tao(0), + fees_alpha=Balance.from_tao(0, netuid=alice_subnet_netuid), + netuid=alice_subnet_netuid, + ) + + liquidity_position_first = liquidity_positions[1] + assert liquidity_position_first == LiquidityPosition( + id=2, + price_low=liquidity_position_first.price_low, + price_high=liquidity_position_first.price_high, + liquidity=Balance.from_tao(10), + fees_tao=liquidity_position_first.fees_tao, + fees_alpha=Balance.from_tao(0, netuid=alice_subnet_netuid), + netuid=alice_subnet_netuid, + ) + # After adding stake alice liquidity position has a fees_tao bc of high price + assert liquidity_position_first.fees_tao > Balance.from_tao(0) + + # Bob remove all stake from alice + assert await async_subtensor.extrinsics.unstake_all( + wallet=bob_wallet, + hotkey=alice_wallet.hotkey.ss58_address, + netuid=alice_subnet_netuid, + rate_tolerance=0.9, # keep high rate tolerance to avoid flaky behavior + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + # Check that fees_alpha comes too after all unstake + liquidity_position_first = ( + await async_subtensor.subnets.get_liquidity_list( + wallet=alice_wallet, netuid=alice_subnet_netuid + ) + )[1] + assert liquidity_position_first.fees_tao > Balance.from_tao(0) + assert liquidity_position_first.fees_alpha > Balance.from_tao( + 0, alice_subnet_netuid + ) + + # Remove all liquidity positions + for p in liquidity_positions: + success, message = await async_subtensor.extrinsics.remove_liquidity( + wallet=alice_wallet, + netuid=alice_subnet_netuid, + position_id=p.id, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert success, message + assert message == "", "❌ Cannot remove liquidity." + + # Make sure all liquidity positions removed + assert ( + await async_subtensor.subnets.get_liquidity_list( + wallet=alice_wallet, netuid=alice_subnet_netuid + ) + == [] + ), "❌ Not all liquidity positions removed." + + logging.console.info("✅ Passed [blue]test_liquidity_async[/blue]") diff --git a/tests/e2e_tests/test_metagraph.py b/tests/e2e_tests/test_metagraph.py new file mode 100644 index 0000000000..d59b944162 --- /dev/null +++ b/tests/e2e_tests/test_metagraph.py @@ -0,0 +1,1386 @@ +import os.path +import re +import shutil +import time +import pytest + +from bittensor.core.chain_data.metagraph_info import MetagraphInfo +from bittensor.core.chain_data import SelectiveMetagraphIndex +from bittensor.utils.balance import Balance +from bittensor.utils.btlogging import logging +from tests.e2e_tests.utils.e2e_test_utils import ( + async_wait_to_start_call, + wait_to_start_call, +) + +NULL_KEY = tuple(bytearray(32)) + + +def neuron_to_dict(neuron): + """ + Convert a neuron object to a dictionary, excluding private attributes, methods, and specific fields. + Returns: + dict: A dictionary of the neuron's public attributes. + + Note: + Excludes 'weights' and 'bonds' fields. These are present in subtensor + but not in metagraph + """ + excluded_fields = {"weights", "bonds"} + return { + attr: getattr(neuron, attr) + for attr in dir(neuron) + if not attr.startswith("_") + and not callable(getattr(neuron, attr)) + and attr not in excluded_fields + } + + +def test_metagraph(subtensor, alice_wallet, bob_wallet, dave_wallet): + """ + Tests the metagraph + + Steps: + 1. Register a subnet through Alice + 2. Assert metagraph's initial state + 3. Register Bob and validate info in metagraph + 4. Fetch neuron info of Bob through subtensor & metagraph and verify + 5. Register Dave and validate info in metagraph + 6. Verify low balance stake fails & add stake thru Bob and verify + 7. Load pre_dave metagraph from latest save and verify both instances + Raises: + AssertionError: If any of the checks or verifications fail + """ + logging.console.info("Testing [blue]test_metagraph[/blue]") + + alice_subnet_netuid = subtensor.subnets.get_total_subnets() # 2 + + logging.console.info("Register the subnet through Alice") + assert subtensor.subnets.register_subnet(alice_wallet), ( + "Unable to register the subnet" + ) + + logging.console.info("Verify subnet was created successfully") + assert subtensor.subnets.subnet_exists(alice_subnet_netuid), ( + "Subnet wasn't created successfully" + ) + + logging.console.info("Make sure we passed start_call limit (10 blocks)") + assert wait_to_start_call(subtensor, alice_wallet, alice_subnet_netuid) + + logging.console.info("Initialize metagraph") + metagraph = subtensor.metagraphs.metagraph(netuid=alice_subnet_netuid) + + logging.console.info("Assert metagraph has only Alice (owner)") + assert len(metagraph.uids) == 1, "Metagraph doesn't have exactly 1 neuron" + + logging.console.info("Register Bob to the subnet") + assert subtensor.subnets.burned_register(bob_wallet, alice_subnet_netuid), ( + "Unable to register Bob as a neuron" + ) + + logging.console.info("Refresh the metagraph") + metagraph.sync(subtensor=subtensor._subtensor) + + logging.console.info("Assert metagraph has Alice and Bob neurons") + assert len(metagraph.uids) == 2, "Metagraph doesn't have exactly 2 neurons" + assert metagraph.hotkeys[0] == alice_wallet.hotkey.ss58_address, ( + "Alice's hotkey doesn't match in metagraph" + ) + assert metagraph.hotkeys[1] == bob_wallet.hotkey.ss58_address, ( + "Bob's hotkey doesn't match in metagraph" + ) + assert len(metagraph.coldkeys) == 2, "Metagraph doesn't have exactly 2 coldkey" + assert metagraph.n.max() == 2, "Metagraph's max n is not 2" + assert metagraph.n.min() == 2, "Metagraph's min n is not 2" + assert len(metagraph.addresses) == 2, "Metagraph doesn't have exactly 2 address" + + logging.console.info("Fetch UID of Bob") + uid = subtensor.subnets.get_uid_for_hotkey_on_subnet( + bob_wallet.hotkey.ss58_address, netuid=alice_subnet_netuid + ) + + logging.console.info("Fetch neuron info of Bob through subtensor and metagraph") + neuron_info_bob = subtensor.neurons.neuron_for_uid(uid, netuid=alice_subnet_netuid) + + metagraph_dict = neuron_to_dict(metagraph.neurons[uid]) + subtensor_dict = neuron_to_dict(neuron_info_bob) + + logging.console.info("Verify neuron info is the same in both objects") + assert metagraph_dict == subtensor_dict, ( + "Neuron info of Bob doesn't match b/w metagraph & subtensor" + ) + + logging.console.info("Create pre_dave metagraph for future verifications") + metagraph_pre_dave = subtensor.metagraphs.metagraph(netuid=alice_subnet_netuid) + + logging.console.info("Register Dave as a neuron") + assert subtensor.subnets.burned_register(dave_wallet, alice_subnet_netuid), ( + "Unable to register Dave as a neuron" + ) + + metagraph.sync(subtensor=subtensor._subtensor) + + logging.console.info("Assert metagraph now includes Dave's neuron") + assert len(metagraph.uids) == 3, ( + "Metagraph doesn't have exactly 3 neurons post Dave" + ) + assert metagraph.hotkeys[2] == dave_wallet.hotkey.ss58_address, ( + "Neuron's hotkey in metagraph doesn't match" + ) + assert len(metagraph.coldkeys) == 3, ( + "Metagraph doesn't have exactly 3 coldkeys post Dave" + ) + assert metagraph.n.max() == 3, "Metagraph's max n is not 3 post Dave" + assert metagraph.n.min() == 3, "Metagraph's min n is not 3 post Dave" + assert len(metagraph.addresses) == 3, "Metagraph doesn't have 3 addresses post Dave" + + logging.console.info("Add stake by Bob") + tao = Balance.from_tao(10_000) + alpha, _ = subtensor.subnets.subnet(alice_subnet_netuid).tao_to_alpha_with_slippage( + tao + ) + assert subtensor.staking.add_stake( + wallet=bob_wallet, + netuid=alice_subnet_netuid, + hotkey_ss58=bob_wallet.hotkey.ss58_address, + amount=tao, + ), "Failed to add stake for Bob" + + logging.console.info("Assert stake is added after updating metagraph") + metagraph.sync(subtensor=subtensor._subtensor) + assert 0.95 < metagraph.neurons[1].stake.rao / alpha.rao < 1.05, ( + "Bob's stake not updated in metagraph" + ) + + logging.console.info("Test the save() and load() mechanism") + # We save the metagraph and pre_dave loads it + # We do this in the /tmp dir to avoid interfering or interacting with user data + metagraph_save_root_dir = ["/", "tmp", "bittensor-e2e", "metagraphs"] + try: + os.makedirs(os.path.join(*metagraph_save_root_dir), exist_ok=True) + metagraph.save(root_dir=metagraph_save_root_dir) + time.sleep(3) + metagraph_pre_dave.load(root_dir=metagraph_save_root_dir) + finally: + shutil.rmtree(os.path.join(*metagraph_save_root_dir)) + + logging.console.info("Ensure data is synced between two metagraphs") + assert len(metagraph.uids) == len(metagraph_pre_dave.uids), ( + "UID count mismatch after save and load" + ) + assert (metagraph.uids == metagraph_pre_dave.uids).all(), ( + "UIDs don't match after save and load" + ) + + assert len(metagraph.axons) == len(metagraph_pre_dave.axons), ( + "Axon count mismatch after save and load" + ) + assert metagraph.axons[1].hotkey == metagraph_pre_dave.axons[1].hotkey, ( + "Axon hotkey mismatch after save and load" + ) + assert metagraph.axons == metagraph_pre_dave.axons, ( + "Axons don't match after save and load" + ) + + assert len(metagraph.neurons) == len(metagraph_pre_dave.neurons), ( + "Neuron count mismatch after save and load" + ) + assert metagraph.neurons == metagraph_pre_dave.neurons, ( + "Neurons don't match after save and load" + ) + + logging.console.info("✅ Passed [blue]test_metagraph[/blue]") + + +@pytest.mark.asyncio +async def test_metagraph_async(async_subtensor, alice_wallet, bob_wallet, dave_wallet): + """ + Async tests the metagraph + + Steps: + 1. Register a subnet through Alice + 2. Assert metagraph's initial state + 3. Register Bob and validate info in metagraph + 4. Fetch neuron info of Bob through subtensor & metagraph and verify + 5. Register Dave and validate info in metagraph + 6. Verify low balance stake fails & add stake thru Bob and verify + 7. Load pre_dave metagraph from latest save and verify both instances + Raises: + AssertionError: If any of the checks or verifications fail + """ + logging.console.info("Testing [blue]test_metagraph_async[/blue]") + + async with async_subtensor: + alice_subnet_netuid = await async_subtensor.subnets.get_total_subnets() # 2 + + logging.console.info("Register the subnet through Alice") + assert await async_subtensor.subnets.register_subnet(alice_wallet), ( + "Unable to register the subnet" + ) + + logging.console.info("Verify subnet was created successfully") + assert await async_subtensor.subnets.subnet_exists(alice_subnet_netuid), ( + "Subnet wasn't created successfully" + ) + + logging.console.info("Make sure we passed start_call limit (10 blocks)") + assert await async_wait_to_start_call( + async_subtensor, alice_wallet, alice_subnet_netuid + ) + + logging.console.info("Initialize metagraph") + metagraph = await async_subtensor.metagraphs.metagraph( + netuid=alice_subnet_netuid + ) + + logging.console.info("Assert metagraph has only Alice (owner)") + assert len(metagraph.uids) == 1, "Metagraph doesn't have exactly 1 neuron" + + logging.console.info("Register Bob to the subnet") + assert await async_subtensor.subnets.burned_register( + bob_wallet, alice_subnet_netuid + ), "Unable to register Bob as a neuron" + + logging.console.info("Refresh the metagraph") + await metagraph.sync(subtensor=async_subtensor._subtensor) + + logging.console.info("Assert metagraph has Alice and Bob neurons") + assert len(metagraph.uids) == 2, "Metagraph doesn't have exactly 2 neurons" + assert metagraph.hotkeys[0] == alice_wallet.hotkey.ss58_address, ( + "Alice's hotkey doesn't match in metagraph" + ) + assert metagraph.hotkeys[1] == bob_wallet.hotkey.ss58_address, ( + "Bob's hotkey doesn't match in metagraph" + ) + assert len(metagraph.coldkeys) == 2, "Metagraph doesn't have exactly 2 coldkey" + assert metagraph.n.max() == 2, "Metagraph's max n is not 2" + assert metagraph.n.min() == 2, "Metagraph's min n is not 2" + assert len(metagraph.addresses) == 2, "Metagraph doesn't have exactly 2 address" + + logging.console.info("Fetch UID of Bob") + uid = await async_subtensor.subnets.get_uid_for_hotkey_on_subnet( + bob_wallet.hotkey.ss58_address, netuid=alice_subnet_netuid + ) + + logging.console.info("Fetch neuron info of Bob through subtensor and metagraph") + neuron_info_bob = await async_subtensor.neurons.neuron_for_uid( + uid, netuid=alice_subnet_netuid + ) + + metagraph_dict = neuron_to_dict(metagraph.neurons[uid]) + subtensor_dict = neuron_to_dict(neuron_info_bob) + + logging.console.info("Verify neuron info is the same in both objects") + assert metagraph_dict == subtensor_dict, ( + "Neuron info of Bob doesn't match b/w metagraph & subtensor" + ) + + logging.console.info("Create pre_dave metagraph for future verifications") + metagraph_pre_dave = await async_subtensor.metagraphs.metagraph( + netuid=alice_subnet_netuid + ) + + logging.console.info("Register Dave as a neuron") + assert await async_subtensor.subnets.burned_register( + dave_wallet, alice_subnet_netuid + ), "Unable to register Dave as a neuron" + + await metagraph.sync(subtensor=async_subtensor._subtensor) + + logging.console.info("Assert metagraph now includes Dave's neuron") + assert len(metagraph.uids) == 3, ( + "Metagraph doesn't have exactly 3 neurons post Dave" + ) + assert metagraph.hotkeys[2] == dave_wallet.hotkey.ss58_address, ( + "Neuron's hotkey in metagraph doesn't match" + ) + assert len(metagraph.coldkeys) == 3, ( + "Metagraph doesn't have exactly 3 coldkeys post Dave" + ) + assert metagraph.n.max() == 3, "Metagraph's max n is not 3 post Dave" + assert metagraph.n.min() == 3, "Metagraph's min n is not 3 post Dave" + assert len(metagraph.addresses) == 3, ( + "Metagraph doesn't have 3 addresses post Dave" + ) + + logging.console.info("Add stake by Bob") + tao = Balance.from_tao(10_000) + alpha, _ = ( + await async_subtensor.subnets.subnet(alice_subnet_netuid) + ).tao_to_alpha_with_slippage(tao) + assert await async_subtensor.staking.add_stake( + wallet=bob_wallet, + netuid=alice_subnet_netuid, + hotkey_ss58=bob_wallet.hotkey.ss58_address, + amount=tao, + ), "Failed to add stake for Bob" + + logging.console.info("Assert stake is added after updating metagraph") + await metagraph.sync(subtensor=async_subtensor._subtensor) + assert 0.95 < metagraph.neurons[1].stake.rao / alpha.rao < 1.05, ( + "Bob's stake not updated in metagraph" + ) + + logging.console.info("Test the save() and load() mechanism") + # We save the metagraph and pre_dave loads it + # We do this in the /tmp dir to avoid interfering or interacting with user data + metagraph_save_root_dir = ["/", "tmp", "bittensor-e2e", "metagraphs"] + try: + os.makedirs(os.path.join(*metagraph_save_root_dir), exist_ok=True) + metagraph.save(root_dir=metagraph_save_root_dir) + time.sleep(3) + metagraph_pre_dave.load(root_dir=metagraph_save_root_dir) + finally: + shutil.rmtree(os.path.join(*metagraph_save_root_dir)) + + logging.console.info("Ensure data is synced between two metagraphs") + assert len(metagraph.uids) == len(metagraph_pre_dave.uids), ( + "UID count mismatch after save and load" + ) + assert (metagraph.uids == metagraph_pre_dave.uids).all(), ( + "UIDs don't match after save and load" + ) + + assert len(metagraph.axons) == len(metagraph_pre_dave.axons), ( + "Axon count mismatch after save and load" + ) + assert metagraph.axons[1].hotkey == metagraph_pre_dave.axons[1].hotkey, ( + "Axon hotkey mismatch after save and load" + ) + assert metagraph.axons == metagraph_pre_dave.axons, ( + "Axons don't match after save and load" + ) + + assert len(metagraph.neurons) == len(metagraph_pre_dave.neurons), ( + "Neuron count mismatch after save and load" + ) + assert metagraph.neurons == metagraph_pre_dave.neurons, ( + "Neurons don't match after save and load" + ) + + logging.console.info("✅ Passed [blue]test_metagraph_async[/blue]") + + +def test_metagraph_info(subtensor, alice_wallet, bob_wallet): + """ + Tests: + - Check MetagraphInfo + - Register Neuron + - Register Subnet + - Check MetagraphInfo is updated + """ + logging.console.info("Testing [blue]test_metagraph_info[/blue]") + + alice_subnet_netuid = subtensor.subnets.get_total_subnets() # 2 + assert subtensor.subnets.register_subnet(alice_wallet) + + metagraph_info = subtensor.metagraphs.get_metagraph_info(netuid=1, block=1) + + expected_metagraph_info = MetagraphInfo( + netuid=1, + name="apex", + symbol="α", + identity=None, + network_registered_at=0, + owner_hotkey="5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM", + owner_coldkey="5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM", + block=1, + tempo=100, + last_step=0, + blocks_since_last_step=1, + subnet_emission=Balance(0), + alpha_in=Balance.from_tao(10).set_unit(1), + alpha_out=Balance.from_tao(1).set_unit(1), + tao_in=Balance.from_tao(10), + alpha_out_emission=Balance(0).set_unit(1), + alpha_in_emission=Balance(0).set_unit(1), + tao_in_emission=Balance(0), + pending_alpha_emission=Balance(0).set_unit(1), + pending_root_emission=Balance(0), + subnet_volume=Balance(0).set_unit(1), + moving_price=Balance(0), + rho=10, + kappa=32767, + min_allowed_weights=0.0, + max_weights_limit=1.0, + weights_version=0, + weights_rate_limit=100, + activity_cutoff=5000, + max_validators=64, + num_uids=1, + max_uids=256, + burn=Balance.from_tao(0.1), + difficulty=5.421010862427522e-13, + registration_allowed=True, + pow_registration_allowed=True, + immunity_period=4096, + min_difficulty=5.421010862427522e-13, + max_difficulty=0.25, + min_burn=Balance.from_tao(0.0005), + max_burn=Balance.from_tao(100), + adjustment_alpha=0.0, + adjustment_interval=100, + target_regs_per_interval=2, + max_regs_per_block=1, + serving_rate_limit=50, + commit_reveal_weights_enabled=True, + commit_reveal_period=1, + liquid_alpha_enabled=False, + alpha_high=0.9000076295109484, + alpha_low=0.7000076295109483, + bonds_moving_avg=4.87890977618477e-14, + hotkeys=["5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM"], + coldkeys=["5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM"], + identities=[None], + axons=( + { + "block": 0, + "version": 0, + "ip": 0, + "port": 0, + "ip_type": 0, + "protocol": 0, + "placeholder1": 0, + "placeholder2": 0, + }, + ), + active=(True,), + validator_permit=(False,), + pruning_score=[0.0], + 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,), + alpha_stake=[Balance.from_tao(1.0).set_unit(1)], + tao_stake=[Balance(0)], + total_stake=[Balance.from_tao(1.0).set_unit(1)], + tao_dividends_per_hotkey=[ + ("5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM", Balance(0)) + ], + alpha_dividends_per_hotkey=[ + ("5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM", Balance(0).set_unit(1)) + ], + validators=None, + ) + + assert metagraph_info == expected_metagraph_info + + metagraph_infos = subtensor.metagraphs.get_all_metagraphs_info(block=1) + + expected_metagraph_infos = [ + MetagraphInfo( + netuid=0, + name="root", + symbol="Τ", + identity=None, + network_registered_at=0, + owner_hotkey="5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM", + owner_coldkey="5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM", + block=1, + tempo=100, + last_step=0, + blocks_since_last_step=1, + subnet_emission=Balance(0), + alpha_in=Balance(0), + alpha_out=Balance(0), + tao_in=Balance(0), + alpha_out_emission=Balance(0), + alpha_in_emission=Balance(0), + tao_in_emission=Balance(0), + pending_alpha_emission=Balance(0), + pending_root_emission=Balance(0), + subnet_volume=Balance(0), + moving_price=Balance(0), + rho=10, + kappa=32767, + min_allowed_weights=0.0, + max_weights_limit=1.0, + weights_version=0, + weights_rate_limit=100, + activity_cutoff=5000, + max_validators=64, + num_uids=0, + max_uids=64, + burn=Balance.from_tao(0.1), + difficulty=5.421010862427522e-13, + registration_allowed=True, + pow_registration_allowed=True, + immunity_period=4096, + min_difficulty=5.421010862427522e-13, + max_difficulty=0.25, + min_burn=Balance.from_tao(0.0005), + max_burn=Balance.from_tao(100), + adjustment_alpha=0.0, + adjustment_interval=100, + target_regs_per_interval=1, + max_regs_per_block=1, + serving_rate_limit=50, + commit_reveal_weights_enabled=True, + commit_reveal_period=1, + liquid_alpha_enabled=False, + alpha_high=0.9000076295109484, + alpha_low=0.7000076295109483, + bonds_moving_avg=4.87890977618477e-14, + hotkeys=[], + coldkeys=[], + identities={}, + axons=(), + active=(), + validator_permit=(), + pruning_score=[], + last_update=(), + emission=[], + dividends=[], + incentives=[], + consensus=[], + trust=[], + rank=[], + block_at_registration=(), + alpha_stake=[], + tao_stake=[], + total_stake=[], + tao_dividends_per_hotkey=[], + alpha_dividends_per_hotkey=[], + validators=None, + ), + metagraph_info, + ] + + assert metagraph_infos == expected_metagraph_infos + + assert wait_to_start_call(subtensor, alice_wallet, alice_subnet_netuid) + + assert subtensor.subnets.burned_register( + bob_wallet, + netuid=alice_subnet_netuid, + ) + + metagraph_info = subtensor.metagraphs.get_metagraph_info(netuid=alice_subnet_netuid) + + assert metagraph_info.num_uids == 2 + assert metagraph_info.hotkeys == [ + alice_wallet.hotkey.ss58_address, + bob_wallet.hotkey.ss58_address, + ] + assert metagraph_info.coldkeys == [ + alice_wallet.coldkey.ss58_address, + bob_wallet.coldkey.ss58_address, + ] + assert metagraph_info.tao_dividends_per_hotkey == [ + ( + alice_wallet.hotkey.ss58_address, + metagraph_info.tao_dividends_per_hotkey[0][1], + ), + (bob_wallet.hotkey.ss58_address, metagraph_info.tao_dividends_per_hotkey[1][1]), + ] + assert metagraph_info.alpha_dividends_per_hotkey == [ + ( + alice_wallet.hotkey.ss58_address, + metagraph_info.alpha_dividends_per_hotkey[0][1], + ), + ( + bob_wallet.hotkey.ss58_address, + metagraph_info.alpha_dividends_per_hotkey[1][1], + ), + ] + + alice_subnet_netuid = subtensor.subnets.get_total_subnets() # 3 + assert subtensor.subnets.register_subnet(alice_wallet) + + block = subtensor.chain.get_current_block() + metagraph_info = subtensor.metagraphs.get_metagraph_info( + netuid=alice_subnet_netuid, block=block + ) + + assert metagraph_info.owner_coldkey == alice_wallet.hotkey.ss58_address + assert metagraph_info.owner_hotkey == alice_wallet.coldkey.ss58_address + + metagraph_infos = subtensor.metagraphs.get_all_metagraphs_info(block) + + assert len(metagraph_infos) == 4 + assert metagraph_infos[-1] == metagraph_info + + # non-existed subnet + metagraph_info = subtensor.metagraphs.get_metagraph_info( + netuid=alice_subnet_netuid + 1 + ) + + assert metagraph_info is None + + logging.console.info("✅ Passed [blue]test_metagraph_info[/blue]") + + +@pytest.mark.asyncio +async def test_metagraph_info_async(async_subtensor, alice_wallet, bob_wallet): + """ + Async tests: + - Check MetagraphInfo + - Register Neuron + - Register Subnet + - Check MetagraphInfo is updated + """ + logging.console.info("Testing [blue]test_metagraph_info_async[/blue]") + + alice_subnet_netuid = await async_subtensor.subnets.get_total_subnets() # 2 + assert await async_subtensor.subnets.register_subnet(alice_wallet) + + metagraph_info = await async_subtensor.metagraphs.get_metagraph_info( + netuid=1, block=1 + ) + + expected_metagraph_info = MetagraphInfo( + netuid=1, + name="apex", + symbol="α", + identity=None, + network_registered_at=0, + owner_hotkey="5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM", + owner_coldkey="5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM", + block=1, + tempo=100, + last_step=0, + blocks_since_last_step=1, + subnet_emission=Balance(0), + alpha_in=Balance.from_tao(10).set_unit(1), + alpha_out=Balance.from_tao(1).set_unit(1), + tao_in=Balance.from_tao(10), + alpha_out_emission=Balance(0).set_unit(1), + alpha_in_emission=Balance(0).set_unit(1), + tao_in_emission=Balance(0), + pending_alpha_emission=Balance(0).set_unit(1), + pending_root_emission=Balance(0), + subnet_volume=Balance(0).set_unit(1), + moving_price=Balance(0), + rho=10, + kappa=32767, + min_allowed_weights=0.0, + max_weights_limit=1.0, + weights_version=0, + weights_rate_limit=100, + activity_cutoff=5000, + max_validators=64, + num_uids=1, + max_uids=256, + burn=Balance.from_tao(0.1), + difficulty=5.421010862427522e-13, + registration_allowed=True, + pow_registration_allowed=True, + immunity_period=4096, + min_difficulty=5.421010862427522e-13, + max_difficulty=0.25, + min_burn=Balance.from_tao(0.0005), + max_burn=Balance.from_tao(100), + adjustment_alpha=0.0, + adjustment_interval=100, + target_regs_per_interval=2, + max_regs_per_block=1, + serving_rate_limit=50, + commit_reveal_weights_enabled=True, + commit_reveal_period=1, + liquid_alpha_enabled=False, + alpha_high=0.9000076295109484, + alpha_low=0.7000076295109483, + bonds_moving_avg=4.87890977618477e-14, + hotkeys=["5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM"], + coldkeys=["5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM"], + identities=[None], + axons=( + { + "block": 0, + "version": 0, + "ip": 0, + "port": 0, + "ip_type": 0, + "protocol": 0, + "placeholder1": 0, + "placeholder2": 0, + }, + ), + active=(True,), + validator_permit=(False,), + pruning_score=[0.0], + 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,), + alpha_stake=[Balance.from_tao(1.0).set_unit(1)], + tao_stake=[Balance(0)], + total_stake=[Balance.from_tao(1.0).set_unit(1)], + tao_dividends_per_hotkey=[ + ("5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM", Balance(0)) + ], + alpha_dividends_per_hotkey=[ + ("5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM", Balance(0).set_unit(1)) + ], + validators=None, + ) + + assert metagraph_info == expected_metagraph_info + + metagraph_infos = await async_subtensor.metagraphs.get_all_metagraphs_info(block=1) + + expected_metagraph_infos = [ + MetagraphInfo( + netuid=0, + name="root", + symbol="Τ", + identity=None, + network_registered_at=0, + owner_hotkey="5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM", + owner_coldkey="5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM", + block=1, + tempo=100, + last_step=0, + blocks_since_last_step=1, + subnet_emission=Balance(0), + alpha_in=Balance(0), + alpha_out=Balance(0), + tao_in=Balance(0), + alpha_out_emission=Balance(0), + alpha_in_emission=Balance(0), + tao_in_emission=Balance(0), + pending_alpha_emission=Balance(0), + pending_root_emission=Balance(0), + subnet_volume=Balance(0), + moving_price=Balance(0), + rho=10, + kappa=32767, + min_allowed_weights=0.0, + max_weights_limit=1.0, + weights_version=0, + weights_rate_limit=100, + activity_cutoff=5000, + max_validators=64, + num_uids=0, + max_uids=64, + burn=Balance.from_tao(0.1), + difficulty=5.421010862427522e-13, + registration_allowed=True, + pow_registration_allowed=True, + immunity_period=4096, + min_difficulty=5.421010862427522e-13, + max_difficulty=0.25, + min_burn=Balance.from_tao(0.0005), + max_burn=Balance.from_tao(100), + adjustment_alpha=0.0, + adjustment_interval=100, + target_regs_per_interval=1, + max_regs_per_block=1, + serving_rate_limit=50, + commit_reveal_weights_enabled=True, + commit_reveal_period=1, + liquid_alpha_enabled=False, + alpha_high=0.9000076295109484, + alpha_low=0.7000076295109483, + bonds_moving_avg=4.87890977618477e-14, + hotkeys=[], + coldkeys=[], + identities={}, + axons=(), + active=(), + validator_permit=(), + pruning_score=[], + last_update=(), + emission=[], + dividends=[], + incentives=[], + consensus=[], + trust=[], + rank=[], + block_at_registration=(), + alpha_stake=[], + tao_stake=[], + total_stake=[], + tao_dividends_per_hotkey=[], + alpha_dividends_per_hotkey=[], + validators=None, + ), + metagraph_info, + ] + + assert metagraph_infos == expected_metagraph_infos + + assert await async_wait_to_start_call( + async_subtensor, alice_wallet, alice_subnet_netuid + ) + + assert await async_subtensor.subnets.burned_register( + bob_wallet, + netuid=alice_subnet_netuid, + ) + + metagraph_info = await async_subtensor.metagraphs.get_metagraph_info( + netuid=alice_subnet_netuid + ) + + assert metagraph_info.num_uids == 2 + assert metagraph_info.hotkeys == [ + alice_wallet.hotkey.ss58_address, + bob_wallet.hotkey.ss58_address, + ] + assert metagraph_info.coldkeys == [ + alice_wallet.coldkey.ss58_address, + bob_wallet.coldkey.ss58_address, + ] + assert metagraph_info.tao_dividends_per_hotkey == [ + ( + alice_wallet.hotkey.ss58_address, + metagraph_info.tao_dividends_per_hotkey[0][1], + ), + (bob_wallet.hotkey.ss58_address, metagraph_info.tao_dividends_per_hotkey[1][1]), + ] + assert metagraph_info.alpha_dividends_per_hotkey == [ + ( + alice_wallet.hotkey.ss58_address, + metagraph_info.alpha_dividends_per_hotkey[0][1], + ), + ( + bob_wallet.hotkey.ss58_address, + metagraph_info.alpha_dividends_per_hotkey[1][1], + ), + ] + + alice_subnet_netuid = await async_subtensor.subnets.get_total_subnets() # 3 + assert await async_subtensor.subnets.register_subnet(alice_wallet) + + block = await async_subtensor.chain.get_current_block() + metagraph_info = await async_subtensor.metagraphs.get_metagraph_info( + netuid=alice_subnet_netuid, block=block + ) + + assert metagraph_info.owner_coldkey == alice_wallet.hotkey.ss58_address + assert metagraph_info.owner_hotkey == alice_wallet.coldkey.ss58_address + + metagraph_infos = await async_subtensor.metagraphs.get_all_metagraphs_info(block) + + assert len(metagraph_infos) == 4 + assert metagraph_infos[-1] == metagraph_info + + # non-existed subnet + metagraph_info = await async_subtensor.metagraphs.get_metagraph_info( + netuid=alice_subnet_netuid + 1 + ) + + assert metagraph_info is None + + logging.console.info("✅ Passed [blue]test_metagraph_info_async[/blue]") + + +def test_metagraph_info_with_indexes(subtensor, alice_wallet, bob_wallet): + """ + Tests: + - Check MetagraphInfo + - Register Neuron + - Register Subnet + - Check MetagraphInfo is updated + """ + logging.console.info("Testing [blue]test_metagraph_info_with_indexes[/blue]") + + alice_subnet_netuid = subtensor.subnets.get_total_subnets() # 2 + assert subtensor.subnets.register_subnet(alice_wallet) + + field_indices = [ + SelectiveMetagraphIndex.Name, + SelectiveMetagraphIndex.Active, + SelectiveMetagraphIndex.OwnerHotkey, + SelectiveMetagraphIndex.OwnerColdkey, + SelectiveMetagraphIndex.Axons, + ] + + metagraph_info = subtensor.metagraphs.get_metagraph_info( + netuid=alice_subnet_netuid, field_indices=field_indices + ) + + assert metagraph_info == MetagraphInfo( + netuid=alice_subnet_netuid, + name="omron", + owner_hotkey=alice_wallet.hotkey.ss58_address, + owner_coldkey=alice_wallet.coldkey.ss58_address, + active=(True,), + axons=( + { + "block": 0, + "ip": 0, + "ip_type": 0, + "placeholder1": 0, + "placeholder2": 0, + "port": 0, + "protocol": 0, + "version": 0, + }, + ), + symbol=None, + identity=None, + network_registered_at=None, + block=None, + tempo=None, + last_step=None, + blocks_since_last_step=None, + subnet_emission=None, + alpha_in=None, + alpha_out=None, + tao_in=None, + alpha_out_emission=None, + alpha_in_emission=None, + tao_in_emission=None, + pending_alpha_emission=None, + pending_root_emission=None, + subnet_volume=None, + moving_price=None, + rho=None, + kappa=None, + min_allowed_weights=None, + max_weights_limit=None, + weights_version=None, + weights_rate_limit=None, + activity_cutoff=None, + max_validators=None, + num_uids=None, + max_uids=None, + burn=None, + difficulty=None, + registration_allowed=None, + pow_registration_allowed=None, + immunity_period=None, + min_difficulty=None, + max_difficulty=None, + min_burn=None, + max_burn=None, + adjustment_alpha=None, + adjustment_interval=None, + target_regs_per_interval=None, + max_regs_per_block=None, + serving_rate_limit=None, + commit_reveal_weights_enabled=None, + commit_reveal_period=None, + liquid_alpha_enabled=None, + alpha_high=None, + alpha_low=None, + bonds_moving_avg=None, + hotkeys=None, + coldkeys=None, + identities=None, + validator_permit=None, + pruning_score=None, + last_update=None, + emission=None, + dividends=None, + incentives=None, + consensus=None, + trust=None, + rank=None, + block_at_registration=None, + alpha_stake=None, + tao_stake=None, + total_stake=None, + tao_dividends_per_hotkey=None, + alpha_dividends_per_hotkey=None, + validators=None, + ) + + assert wait_to_start_call(subtensor, alice_wallet, alice_subnet_netuid) + + assert subtensor.subnets.burned_register( + wallet=bob_wallet, + netuid=alice_subnet_netuid, + ) + + fields = [ + SelectiveMetagraphIndex.Name, + SelectiveMetagraphIndex.Active, + SelectiveMetagraphIndex.OwnerHotkey, + SelectiveMetagraphIndex.OwnerColdkey, + SelectiveMetagraphIndex.Axons, + ] + + metagraph_info = subtensor.metagraphs.get_metagraph_info( + netuid=alice_subnet_netuid, field_indices=fields + ) + + assert metagraph_info == MetagraphInfo( + netuid=alice_subnet_netuid, + name="omron", + owner_hotkey=alice_wallet.hotkey.ss58_address, + owner_coldkey=alice_wallet.coldkey.ss58_address, + active=(True, True), + axons=( + { + "block": 0, + "ip": 0, + "ip_type": 0, + "placeholder1": 0, + "placeholder2": 0, + "port": 0, + "protocol": 0, + "version": 0, + }, + { + "block": 0, + "ip": 0, + "ip_type": 0, + "placeholder1": 0, + "placeholder2": 0, + "port": 0, + "protocol": 0, + "version": 0, + }, + ), + symbol=None, + identity=None, + network_registered_at=None, + block=None, + tempo=None, + last_step=None, + blocks_since_last_step=None, + subnet_emission=None, + alpha_in=None, + alpha_out=None, + tao_in=None, + alpha_out_emission=None, + alpha_in_emission=None, + tao_in_emission=None, + pending_alpha_emission=None, + pending_root_emission=None, + subnet_volume=None, + moving_price=None, + rho=None, + kappa=None, + min_allowed_weights=None, + max_weights_limit=None, + weights_version=None, + weights_rate_limit=None, + activity_cutoff=None, + max_validators=None, + num_uids=None, + max_uids=None, + burn=None, + difficulty=None, + registration_allowed=None, + pow_registration_allowed=None, + immunity_period=None, + min_difficulty=None, + max_difficulty=None, + min_burn=None, + max_burn=None, + adjustment_alpha=None, + adjustment_interval=None, + target_regs_per_interval=None, + max_regs_per_block=None, + serving_rate_limit=None, + commit_reveal_weights_enabled=None, + commit_reveal_period=None, + liquid_alpha_enabled=None, + alpha_high=None, + alpha_low=None, + bonds_moving_avg=None, + hotkeys=None, + coldkeys=None, + identities=None, + validator_permit=None, + pruning_score=None, + last_update=None, + emission=None, + dividends=None, + incentives=None, + consensus=None, + trust=None, + rank=None, + block_at_registration=None, + alpha_stake=None, + tao_stake=None, + total_stake=None, + tao_dividends_per_hotkey=None, + alpha_dividends_per_hotkey=None, + validators=None, + ) + + logging.console.info("✅ Passed [blue]test_metagraph_info_with_indexes[/blue]") + + +@pytest.mark.asyncio +async def test_metagraph_info_with_indexes_async( + async_subtensor, alice_wallet, bob_wallet +): + """ + Async tests: + - Check MetagraphInfo + - Register Neuron + - Register Subnet + - Check MetagraphInfo is updated + """ + logging.console.info("Testing [blue]test_metagraph_info_with_indexes_async[/blue]") + + alice_subnet_netuid = await async_subtensor.subnets.get_total_subnets() # 2 + assert await async_subtensor.subnets.register_subnet(alice_wallet) + + field_indices = [ + SelectiveMetagraphIndex.Name, + SelectiveMetagraphIndex.Active, + SelectiveMetagraphIndex.OwnerHotkey, + SelectiveMetagraphIndex.OwnerColdkey, + SelectiveMetagraphIndex.Axons, + ] + + metagraph_info = await async_subtensor.metagraphs.get_metagraph_info( + netuid=alice_subnet_netuid, field_indices=field_indices + ) + + assert metagraph_info == MetagraphInfo( + netuid=alice_subnet_netuid, + name="omron", + owner_hotkey=alice_wallet.hotkey.ss58_address, + owner_coldkey=alice_wallet.coldkey.ss58_address, + active=(True,), + axons=( + { + "block": 0, + "ip": 0, + "ip_type": 0, + "placeholder1": 0, + "placeholder2": 0, + "port": 0, + "protocol": 0, + "version": 0, + }, + ), + symbol=None, + identity=None, + network_registered_at=None, + block=None, + tempo=None, + last_step=None, + blocks_since_last_step=None, + subnet_emission=None, + alpha_in=None, + alpha_out=None, + tao_in=None, + alpha_out_emission=None, + alpha_in_emission=None, + tao_in_emission=None, + pending_alpha_emission=None, + pending_root_emission=None, + subnet_volume=None, + moving_price=None, + rho=None, + kappa=None, + min_allowed_weights=None, + max_weights_limit=None, + weights_version=None, + weights_rate_limit=None, + activity_cutoff=None, + max_validators=None, + num_uids=None, + max_uids=None, + burn=None, + difficulty=None, + registration_allowed=None, + pow_registration_allowed=None, + immunity_period=None, + min_difficulty=None, + max_difficulty=None, + min_burn=None, + max_burn=None, + adjustment_alpha=None, + adjustment_interval=None, + target_regs_per_interval=None, + max_regs_per_block=None, + serving_rate_limit=None, + commit_reveal_weights_enabled=None, + commit_reveal_period=None, + liquid_alpha_enabled=None, + alpha_high=None, + alpha_low=None, + bonds_moving_avg=None, + hotkeys=None, + coldkeys=None, + identities=None, + validator_permit=None, + pruning_score=None, + last_update=None, + emission=None, + dividends=None, + incentives=None, + consensus=None, + trust=None, + rank=None, + block_at_registration=None, + alpha_stake=None, + tao_stake=None, + total_stake=None, + tao_dividends_per_hotkey=None, + alpha_dividends_per_hotkey=None, + validators=None, + ) + + assert await async_wait_to_start_call( + async_subtensor, alice_wallet, alice_subnet_netuid + ) + + assert await async_subtensor.subnets.burned_register( + wallet=bob_wallet, + netuid=alice_subnet_netuid, + ) + + fields = [ + SelectiveMetagraphIndex.Name, + SelectiveMetagraphIndex.Active, + SelectiveMetagraphIndex.OwnerHotkey, + SelectiveMetagraphIndex.OwnerColdkey, + SelectiveMetagraphIndex.Axons, + ] + + metagraph_info = await async_subtensor.metagraphs.get_metagraph_info( + netuid=alice_subnet_netuid, field_indices=fields + ) + + assert metagraph_info == MetagraphInfo( + netuid=alice_subnet_netuid, + name="omron", + owner_hotkey=alice_wallet.hotkey.ss58_address, + owner_coldkey=alice_wallet.coldkey.ss58_address, + active=(True, True), + axons=( + { + "block": 0, + "ip": 0, + "ip_type": 0, + "placeholder1": 0, + "placeholder2": 0, + "port": 0, + "protocol": 0, + "version": 0, + }, + { + "block": 0, + "ip": 0, + "ip_type": 0, + "placeholder1": 0, + "placeholder2": 0, + "port": 0, + "protocol": 0, + "version": 0, + }, + ), + symbol=None, + identity=None, + network_registered_at=None, + block=None, + tempo=None, + last_step=None, + blocks_since_last_step=None, + subnet_emission=None, + alpha_in=None, + alpha_out=None, + tao_in=None, + alpha_out_emission=None, + alpha_in_emission=None, + tao_in_emission=None, + pending_alpha_emission=None, + pending_root_emission=None, + subnet_volume=None, + moving_price=None, + rho=None, + kappa=None, + min_allowed_weights=None, + max_weights_limit=None, + weights_version=None, + weights_rate_limit=None, + activity_cutoff=None, + max_validators=None, + num_uids=None, + max_uids=None, + burn=None, + difficulty=None, + registration_allowed=None, + pow_registration_allowed=None, + immunity_period=None, + min_difficulty=None, + max_difficulty=None, + min_burn=None, + max_burn=None, + adjustment_alpha=None, + adjustment_interval=None, + target_regs_per_interval=None, + max_regs_per_block=None, + serving_rate_limit=None, + commit_reveal_weights_enabled=None, + commit_reveal_period=None, + liquid_alpha_enabled=None, + alpha_high=None, + alpha_low=None, + bonds_moving_avg=None, + hotkeys=None, + coldkeys=None, + identities=None, + validator_permit=None, + pruning_score=None, + last_update=None, + emission=None, + dividends=None, + incentives=None, + consensus=None, + trust=None, + rank=None, + block_at_registration=None, + alpha_stake=None, + tao_stake=None, + total_stake=None, + tao_dividends_per_hotkey=None, + alpha_dividends_per_hotkey=None, + validators=None, + ) + + logging.console.info( + "✅ Passed [blue]test_metagraph_info_with_indexes_async[/blue]" + ) + + +def test_blocks(subtensor): + """ + Tests: + - Get current block + - Get block hash + - Wait for block + """ + logging.console.info("Testing [blue]test_blocks[/blue]") + + block = subtensor.chain.get_current_block() + assert block == subtensor.block + + block_hash = subtensor.chain.get_block_hash(block) + assert re.match("0x[a-z0-9]{64}", block_hash) + + subtensor.wait_for_block(block + 10) + assert subtensor.chain.get_current_block() == block + 10 + + logging.console.info("✅ Passed [blue]test_blocks[/blue]") + + +@pytest.mark.asyncio +async def test_blocks_async(subtensor): + """ + Async tests: + - Get current block + - Get block hash + - Wait for block + """ + logging.console.info("Testing [blue]test_blocks_async[/blue]") + + block = subtensor.chain.get_current_block() + assert block == subtensor.block + + block_hash = subtensor.chain.get_block_hash(block) + assert re.match("0x[a-z0-9]{64}", block_hash) + + subtensor.wait_for_block(block + 10) + assert subtensor.chain.get_current_block() == block + 10 + logging.console.info("✅ Passed [blue]test_blocks_async[/blue]") diff --git a/tests/e2e_tests/test_neuron_certificate.py b/tests/e2e_tests/test_neuron_certificate.py new file mode 100644 index 0000000000..0a1748d8eb --- /dev/null +++ b/tests/e2e_tests/test_neuron_certificate.py @@ -0,0 +1,111 @@ +import pytest +from bittensor.core.axon import Axon +from bittensor.utils.btlogging import logging + + +@pytest.mark.asyncio +async def test_neuron_certificate(subtensor, alice_wallet): + """ + Tests the metagraph + + Steps: + 1. Register a subnet through Alice + 2. Serve Alice axon with neuron certificate + 3. Verify neuron certificate can be retrieved + Raises: + AssertionError: If any of the checks or verifications fail + """ + logging.info("Testing [blue]neuron_certificate[/blue]") + netuid = 2 + + # Register root as Alice - the subnet owner and validator + assert subtensor.subnets.register_subnet(alice_wallet) + + # Verify subnet created successfully + assert subtensor.subnets.subnet_exists(netuid), "Subnet wasn't created successfully" + + # Register Alice as a neuron on the subnet + assert subtensor.subnets.burned_register(alice_wallet, netuid), ( + "Unable to register Alice as a neuron" + ) + + # Serve Alice's axon with a certificate + axon = Axon(wallet=alice_wallet) + encoded_certificate = "?FAKE_ALICE_CERT" + subtensor.extrinsics.serve_axon( + netuid, + axon, + certificate=encoded_certificate, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + # Verify we are getting the correct certificate + assert ( + subtensor.neurons.get_neuron_certificate( + netuid=netuid, + hotkey=alice_wallet.hotkey.ss58_address, + ) + == encoded_certificate + ) + all_certs_query = subtensor.neurons.get_all_neuron_certificates(netuid=netuid) + assert alice_wallet.hotkey.ss58_address in all_certs_query.keys() + assert all_certs_query[alice_wallet.hotkey.ss58_address] == encoded_certificate + + logging.console.success("✅ Passed [blue]test_neuron_certificate[/blue]") + + +@pytest.mark.asyncio +async def test_neuron_certificate_async(async_subtensor, alice_wallet): + """ + ASync tests the metagraph + + Steps: + 1. Register a subnet through Alice + 2. Serve Alice axon with neuron certificate + 3. Verify neuron certificate can be retrieved + Raises: + AssertionError: If any of the checks or verifications fail + """ + logging.info("Testing [blue]neuron_certificate[/blue]") + netuid = 2 + + # Register root as Alice - the subnet owner and validator + assert await async_subtensor.subnets.register_subnet(alice_wallet) + + # Verify subnet created successfully + assert await async_subtensor.subnets.subnet_exists(netuid), ( + "Subnet wasn't created successfully" + ) + + # Register Alice as a neuron on the subnet + assert await async_subtensor.subnets.burned_register(alice_wallet, netuid), ( + "Unable to register Alice as a neuron" + ) + + # Serve Alice's axon with a certificate + axon = Axon(wallet=alice_wallet) + encoded_certificate = "?FAKE_ALICE_CERT" + await async_subtensor.extrinsics.serve_axon( + netuid, + axon, + certificate=encoded_certificate, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + # Verify we are getting the correct certificate + assert ( + await async_subtensor.neurons.get_neuron_certificate( + netuid=netuid, + hotkey=alice_wallet.hotkey.ss58_address, + ) + == encoded_certificate + ) + all_certs_query = await async_subtensor.neurons.get_all_neuron_certificates( + netuid=netuid + ) + assert alice_wallet.hotkey.ss58_address in all_certs_query.keys() + assert all_certs_query[alice_wallet.hotkey.ss58_address] == encoded_certificate + + logging.console.success("✅ Passed [blue]test_neuron_certificate[/blue]") diff --git a/tests/e2e_tests/test_reveal_commitments.py b/tests/e2e_tests/test_reveal_commitments.py new file mode 100644 index 0000000000..9ad12eaa76 --- /dev/null +++ b/tests/e2e_tests/test_reveal_commitments.py @@ -0,0 +1,245 @@ +import time + +import pytest + +from bittensor.utils.btlogging import logging +from tests.e2e_tests.utils.e2e_test_utils import ( + async_wait_to_start_call, + wait_to_start_call, +) + + +def test_set_reveal_commitment(subtensor, alice_wallet, bob_wallet): + """ + Tests the set/reveal commitments with TLE (time-locked encrypted commitments) mechanism. + + Steps: + 1. Register a subnet through Alice + 2. Register Bob's neuron and add stake + 3. Set commitment from Alice hotkey + 4. Set commitment from Bob hotkey + 5. Wait until commitment is revealed. + 5. Verify commitment is revealed by Alice and Bob and available via mutual call. + 6. Verify commitment is revealed by Alice and Bob and available via separate calls. + Raises: + AssertionError: If any of the checks or verifications fail + + Note: Actually we can run this tests in fast block mode. For this we need to set `BLOCK_TIME` to 0.25 and replace + `False` to `True` in `pytest.mark.parametrize` decorator. + """ + logging.console.info("Testing [blue]test_set_reveal_commitment[/blue]") + + BLOCK_TIME, BLOCKS_UNTIL_REVEAL = ( + (0.25, 10) if subtensor.chain.is_fast_blocks() else (12.0, 5) + ) + + alice_subnet_netuid = subtensor.subnets.get_total_subnets() # 2 + + logging.console.info("Testing Drand encrypted commitments.") + + # Register subnet as Alice + assert subtensor.subnets.register_subnet(alice_wallet), ( + "Unable to register the subnet" + ) + + assert wait_to_start_call(subtensor, alice_wallet, alice_subnet_netuid) + + # Register Bob's neuron + assert subtensor.subnets.burned_register( + wallet=bob_wallet, + netuid=alice_subnet_netuid, + ), "Bob's neuron was not register." + + # Verify subnet 2 created successfully + assert subtensor.subnets.subnet_exists(alice_subnet_netuid), ( + "Subnet wasn't created successfully" + ) + + # Set commitment from Alice hotkey + message_alice = f"This is test message with time {time.time()} from Alice." + + response = subtensor.commitments.set_reveal_commitment( + alice_wallet, + alice_subnet_netuid, + message_alice, + BLOCKS_UNTIL_REVEAL, + BLOCK_TIME, + ) + assert response[0] is True + + # Set commitment from Bob's hotkey + message_bob = f"This is test message with time {time.time()} from Bob." + + response = subtensor.commitments.set_reveal_commitment( + bob_wallet, + alice_subnet_netuid, + message_bob, + BLOCKS_UNTIL_REVEAL, + block_time=BLOCK_TIME, + ) + assert response[0] is True + + target_reveal_round = response[1] + + # Sometimes the chain doesn't update the repository right away and the commit doesn't appear in the expected + # `last_drand_round`. In this case need to wait a bit. + print(f"Waiting for reveal round {target_reveal_round}") + chain_offset = 1 if subtensor.chain.is_fast_blocks() else 24 + + last_drand_round = -1 + while last_drand_round <= target_reveal_round + chain_offset: + # wait one drand period (3 sec) + last_drand_round = subtensor.chain.last_drand_round() + print(f"Current last reveled drand round {last_drand_round}") + time.sleep(3) + + actual_all = subtensor.commitments.get_all_revealed_commitments(alice_subnet_netuid) + + alice_result = actual_all.get(alice_wallet.hotkey.ss58_address) + assert alice_result is not None, "Alice's commitment was not received." + + bob_result = actual_all.get(bob_wallet.hotkey.ss58_address) + assert bob_result is not None, "Bob's commitment was not received." + + alice_actual_block, alice_actual_message = alice_result[0] + bob_actual_block, bob_actual_message = bob_result[0] + + # We do not check the release block because it is a dynamic number. It depends on the load of the chain, the number + # of commits in the chain and the computing power. + assert message_alice == alice_actual_message + assert message_bob == bob_actual_message + + # Assertions for get_revealed_commitment (based of hotkey) + actual_alice_block, actual_alice_message = ( + subtensor.commitments.get_revealed_commitment(alice_subnet_netuid, 0)[0] + ) + actual_bob_block, actual_bob_message = ( + subtensor.commitments.get_revealed_commitment(alice_subnet_netuid, 1)[0] + ) + + assert message_alice == actual_alice_message + assert message_bob == actual_bob_message + + logging.console.success("✅ Passed [blue]test_set_reveal_commitment[/blue]") + + +@pytest.mark.asyncio +async def test_set_reveal_commitment(async_subtensor, alice_wallet, bob_wallet): + """ + Tests the set/reveal commitments with TLE (time-locked encrypted commitments) mechanism. + + Steps: + 1. Register a subnet through Alice + 2. Register Bob's neuron and add stake + 3. Set commitment from Alice hotkey + 4. Set commitment from Bob hotkey + 5. Wait until commitment is revealed. + 5. Verify commitment is revealed by Alice and Bob and available via mutual call. + 6. Verify commitment is revealed by Alice and Bob and available via separate calls. + Raises: + AssertionError: If any of the checks or verifications fail + + Note: Actually we can run this tests in fast block mode. For this we need to set `BLOCK_TIME` to 0.25 and replace + `False` to `True` in `pytest.mark.parametrize` decorator. + """ + logging.console.info("Testing [blue]test_set_reveal_commitment[/blue]") + + BLOCK_TIME, BLOCKS_UNTIL_REVEAL = ( + (0.25, 10) if await async_subtensor.chain.is_fast_blocks() else (12.0, 5) + ) + + alice_subnet_netuid = await async_subtensor.subnets.get_total_subnets() # 2 + + logging.console.info("Testing Drand encrypted commitments.") + + # Register subnet as Alice + assert await async_subtensor.subnets.register_subnet(alice_wallet), ( + "Unable to register the subnet" + ) + + assert await async_wait_to_start_call( + async_subtensor, alice_wallet, alice_subnet_netuid + ) + + # Register Bob's neuron + assert await async_subtensor.subnets.burned_register( + bob_wallet, alice_subnet_netuid + ), "Bob's neuron was not register." + + # Verify subnet 2 created successfully + assert await async_subtensor.subnets.subnet_exists(alice_subnet_netuid), ( + "Subnet wasn't created successfully" + ) + + # Set commitment from Alice hotkey + message_alice = f"This is test message with time {time.time()} from Alice." + + response = await async_subtensor.commitments.set_reveal_commitment( + alice_wallet, + alice_subnet_netuid, + message_alice, + BLOCKS_UNTIL_REVEAL, + BLOCK_TIME, + ) + assert response[0] is True + + # Set commitment from Bob's hotkey + message_bob = f"This is test message with time {time.time()} from Bob." + + response = await async_subtensor.commitments.set_reveal_commitment( + bob_wallet, + alice_subnet_netuid, + message_bob, + BLOCKS_UNTIL_REVEAL, + block_time=BLOCK_TIME, + ) + assert response[0] is True + + target_reveal_round = response[1] + + # Sometimes the chain doesn't update the repository right away and the commit doesn't appear in the expected + # `last_drand_round`. In this case need to wait a bit. + print(f"Waiting for reveal round {target_reveal_round}") + chain_offset = 1 if await async_subtensor.chain.is_fast_blocks() else 24 + + last_drand_round = -1 + while last_drand_round <= target_reveal_round + chain_offset: + # wait one drand period (3 sec) + last_drand_round = await async_subtensor.chain.last_drand_round() + print(f"Current last reveled drand round {last_drand_round}") + time.sleep(3) + + actual_all = await async_subtensor.commitments.get_all_revealed_commitments( + alice_subnet_netuid + ) + + alice_result = actual_all.get(alice_wallet.hotkey.ss58_address) + assert alice_result is not None, "Alice's commitment was not received." + + bob_result = actual_all.get(bob_wallet.hotkey.ss58_address) + assert bob_result is not None, "Bob's commitment was not received." + + alice_actual_block, alice_actual_message = alice_result[0] + bob_actual_block, bob_actual_message = bob_result[0] + + # We do not check the release block because it is a dynamic number. It depends on the load of the chain, the number + # of commits in the chain and the computing power. + assert message_alice == alice_actual_message + assert message_bob == bob_actual_message + + # Assertions for get_revealed_commitment (based of hotkey) + actual_alice_block, actual_alice_message = ( + await async_subtensor.commitments.get_revealed_commitment( + alice_subnet_netuid, 0 + ) + )[0] + actual_bob_block, actual_bob_message = ( + await async_subtensor.commitments.get_revealed_commitment( + alice_subnet_netuid, 1 + ) + )[0] + + assert message_alice == actual_alice_message + assert message_bob == actual_bob_message + + logging.console.success("✅ Passed [blue]test_set_reveal_commitment[/blue]") diff --git a/tests/e2e_tests/test_root_set_weights.py b/tests/e2e_tests/test_root_set_weights.py new file mode 100644 index 0000000000..7d18dccfcf --- /dev/null +++ b/tests/e2e_tests/test_root_set_weights.py @@ -0,0 +1,280 @@ +import asyncio + +import pytest + +from bittensor.utils.balance import Balance +from bittensor.utils.btlogging import logging +from tests.e2e_tests.utils.chain_interactions import ( + async_wait_epoch, + async_sudo_set_hyperparameter_values, + wait_epoch, + sudo_set_hyperparameter_values, +) +from tests.e2e_tests.utils.e2e_test_utils import ( + async_wait_to_start_call, + wait_to_start_call, +) + +FAST_BLOCKS_SPEEDUP_FACTOR = 5 + +""" +Verifies: + +* root_register() +* neurons() +* register_subnet() +* burned_register() +* immunity_period() +* tempo() +* get_uid_for_hotkey_on_subnet() +* blocks_since_last_update() +* subnetwork_n() +* min_allowed_weights() +* max_weight_limit() +* weights_rate_limit() +* root_set_weights() +* neurons_lite() +""" + + +@pytest.mark.asyncio +async def test_root_reg_hyperparams(subtensor, templates, alice_wallet, bob_wallet): + """ + Test root weights and hyperparameters in the Subtensor network. + + Steps: + 1. Register Alice in the root network (netuid=0). + 2. Create a new subnet (netuid=1) and register Alice on this subnet using burned registration. + 3. Verify that the subnet's `immunity_period` and `tempo` match the default values. + 4. Run Alice as a validator in the background. + 5. Fetch Alice's UID on the subnet and record the blocks since her last update. + 6. Verify that the subnet was created successfully by checking `subnetwork_n`. + 7. Verify hyperparameters related to weights: `min_allowed_weights`, `max_weight_limit`, and `weights_rate_limit`. + 8. Wait until the next epoch and set root weights for netuids 0 and 1. + 9. Verify that the weights are correctly set on the chain. + 10. Adjust hyperparameters to allow proof-of-work (PoW) registration. + 11. Verify that the `blocks_since_last_update` has incremented. + 12. Fetch neurons using `neurons_lite` for the subnet and verify Alice's participation. + + Raises: + AssertionError: If any of the checks or verifications fail. + """ + + logging.console.info("Testing root register, weights, and hyperparams") + netuid = subtensor.subnets.get_total_subnets() # 2 + + # Default immunity period and tempo set through the subtensor side + default_immunity_period = 5000 + default_tempo = 10 if subtensor.chain.is_fast_blocks() else 360 + + # Register Alice in root network (0) + assert subtensor.extrinsics.root_register(alice_wallet) + + # Assert Alice is successfully registered to root + alice_root_neuron = subtensor.neurons.neurons(netuid=0)[0] + assert alice_root_neuron.coldkey == alice_wallet.coldkeypub.ss58_address + assert alice_root_neuron.hotkey == alice_wallet.hotkey.ss58_address + + # Create netuid = 2 + assert subtensor.subnets.register_subnet(alice_wallet) + + assert wait_to_start_call( + subtensor=subtensor, subnet_owner_wallet=alice_wallet, netuid=netuid + ) + + # Ensure correct immunity period and tempo is being fetched + assert subtensor.subnets.immunity_period(netuid=netuid) == default_immunity_period + assert subtensor.subnets.tempo(netuid=netuid) == default_tempo + + assert subtensor.staking.add_stake( + wallet=bob_wallet, + netuid=netuid, + hotkey_ss58=alice_wallet.hotkey.ss58_address, + amount=Balance.from_tao(1), + period=16, + ), "Unable to stake from Bob to Alice" + + async with templates.validator(alice_wallet, netuid): + await asyncio.sleep(5) # Wait a bit for chain to process data + + # Fetch uid against Alice's hotkey on sn 2 (it will be 0 as she is the only registered neuron) + alice_uid_sn_2 = subtensor.subnets.get_uid_for_hotkey_on_subnet( + alice_wallet.hotkey.ss58_address, netuid + ) + + # Fetch the block since last update for the neuron + block_since_update = subtensor.subnets.blocks_since_last_update( + netuid=netuid, uid=alice_uid_sn_2 + ) + assert block_since_update is not None + + # Verify subnet created successfully + assert subtensor.subnets.subnet_exists(netuid), "Subnet wasn't created successfully" + + # Use subnetwork_n hyperparam to check sn creation + assert subtensor.subnets.subnetwork_n(netuid) == 1 # TODO? + assert subtensor.subnets.subnetwork_n(netuid + 1) is None + + # Ensure correct hyperparams are being fetched regarding weights + assert subtensor.subnets.min_allowed_weights(netuid) is not None + assert subtensor.subnets.max_weight_limit(netuid) is not None + assert subtensor.subnets.weights_rate_limit(netuid) is not None + + # Wait until next epoch so we can set root weights + await wait_epoch(subtensor, netuid) + + # Change hyperparams so we can execute pow_register + assert sudo_set_hyperparameter_values( + substrate=subtensor.substrate, + wallet=alice_wallet, + call_function="sudo_set_difficulty", + call_params={"netuid": netuid, "difficulty": "1_000_000"}, + return_error_message=True, + ) + + assert sudo_set_hyperparameter_values( + substrate=subtensor.substrate, + wallet=alice_wallet, + call_function="sudo_set_network_pow_registration_allowed", + call_params={"netuid": netuid, "registration_allowed": True}, + return_error_message=True, + ) + + # TODO: Implement + # This registers neuron using pow but it doesn't work on fast-blocks - we get stale pow + # pow_registration = subtensor.register(bob_wallet, netuid=1) + + # Fetch neuron lite for sn one and assert Alice participation + sn_one_neurons = subtensor.neurons.neurons_lite(netuid=netuid) + assert ( + sn_one_neurons[alice_uid_sn_2].coldkey == alice_wallet.coldkeypub.ss58_address + ) + assert sn_one_neurons[alice_uid_sn_2].hotkey == alice_wallet.hotkey.ss58_address + assert sn_one_neurons[alice_uid_sn_2].validator_permit is True + + logging.console.success("✅ Passed root tests") + + +@pytest.mark.asyncio +async def test_root_reg_hyperparams_async( + async_subtensor, templates, alice_wallet, bob_wallet +): + """ + Async test root weights and hyperparameters in the Subtensor network. + + Steps: + 1. Register Alice in the root network (netuid=0). + 2. Create a new subnet (netuid=1) and register Alice on this subnet using burned registration. + 3. Verify that the subnet's `immunity_period` and `tempo` match the default values. + 4. Run Alice as a validator in the background. + 5. Fetch Alice's UID on the subnet and record the blocks since her last update. + 6. Verify that the subnet was created successfully by checking `subnetwork_n`. + 7. Verify hyperparameters related to weights: `min_allowed_weights`, `max_weight_limit`, and `weights_rate_limit`. + 8. Wait until the next epoch and set root weights for netuids 0 and 1. + 9. Verify that the weights are correctly set on the chain. + 10. Adjust hyperparameters to allow proof-of-work (PoW) registration. + 11. Verify that the `blocks_since_last_update` has incremented. + 12. Fetch neurons using `neurons_lite` for the subnet and verify Alice's participation. + + Raises: + AssertionError: If any of the checks or verifications fail. + """ + + logging.console.info("Testing root register, weights, and hyperparams") + netuid = await async_subtensor.subnets.get_total_subnets() # 2 + + # Default immunity period and tempo set through the subtensor side + default_immunity_period = 5000 + default_tempo = 10 if await async_subtensor.chain.is_fast_blocks() else 360 + + # Register Alice in root network (0) + assert await async_subtensor.extrinsics.root_register(alice_wallet) + + # Assert Alice is successfully registered to root + alice_root_neuron = (await async_subtensor.neurons.neurons(netuid=0))[0] + assert alice_root_neuron.coldkey == alice_wallet.coldkeypub.ss58_address + assert alice_root_neuron.hotkey == alice_wallet.hotkey.ss58_address + + # Create netuid = 2 + assert await async_subtensor.subnets.register_subnet(alice_wallet) + + assert await async_wait_to_start_call( + subtensor=async_subtensor, subnet_owner_wallet=alice_wallet, netuid=netuid + ) + + # Ensure correct immunity period and tempo is being fetched + assert ( + await async_subtensor.subnets.immunity_period(netuid=netuid) + == default_immunity_period + ) + assert await async_subtensor.subnets.tempo(netuid=netuid) == default_tempo + + assert await async_subtensor.staking.add_stake( + wallet=bob_wallet, + netuid=netuid, + hotkey_ss58=alice_wallet.hotkey.ss58_address, + amount=Balance.from_tao(1), + period=16, + ), "Unable to stake from Bob to Alice" + + async with templates.validator(alice_wallet, netuid): + await asyncio.sleep(5) # Wait a bit for chain to process data + + # Fetch uid against Alice's hotkey on sn 2 (it will be 0 as she is the only registered neuron) + alice_uid_sn_2 = await async_subtensor.subnets.get_uid_for_hotkey_on_subnet( + alice_wallet.hotkey.ss58_address, netuid + ) + + # Fetch the block since last update for the neuron + block_since_update = await async_subtensor.subnets.blocks_since_last_update( + netuid=netuid, uid=alice_uid_sn_2 + ) + assert block_since_update is not None + + # Verify subnet created successfully + assert await async_subtensor.subnets.subnet_exists(netuid), ( + "Subnet wasn't created successfully" + ) + + # Use subnetwork_n hyperparam to check sn creation + assert await async_subtensor.subnets.subnetwork_n(netuid) == 1 # TODO? + assert await async_subtensor.subnets.subnetwork_n(netuid + 1) is None + + # Ensure correct hyperparams are being fetched regarding weights + assert await async_subtensor.subnets.min_allowed_weights(netuid) is not None + assert await async_subtensor.subnets.max_weight_limit(netuid) is not None + assert await async_subtensor.subnets.weights_rate_limit(netuid) is not None + + # Wait until next epoch so we can set root weights + await async_wait_epoch(async_subtensor, netuid) + + # Change hyperparams so we can execute pow_register + assert await async_sudo_set_hyperparameter_values( + substrate=async_subtensor.substrate, + wallet=alice_wallet, + call_function="sudo_set_difficulty", + call_params={"netuid": netuid, "difficulty": "1_000_000"}, + return_error_message=True, + ) + + assert await async_sudo_set_hyperparameter_values( + substrate=async_subtensor.substrate, + wallet=alice_wallet, + call_function="sudo_set_network_pow_registration_allowed", + call_params={"netuid": netuid, "registration_allowed": True}, + return_error_message=True, + ) + + # TODO: Implement + # This registers neuron using pow but it doesn't work on fast-blocks - we get stale pow + # pow_registration = subtensor.register(bob_wallet, netuid=1) + + # Fetch neuron lite for sn one and assert Alice participation + sn_one_neurons = await async_subtensor.neurons.neurons_lite(netuid=netuid) + assert ( + sn_one_neurons[alice_uid_sn_2].coldkey == alice_wallet.coldkeypub.ss58_address + ) + assert sn_one_neurons[alice_uid_sn_2].hotkey == alice_wallet.hotkey.ss58_address + assert sn_one_neurons[alice_uid_sn_2].validator_permit is True + + logging.console.success("✅ Passed root tests") diff --git a/tests/e2e_tests/test_set_subnet_identity_extrinsic.py b/tests/e2e_tests/test_set_subnet_identity_extrinsic.py new file mode 100644 index 0000000000..de52ad4550 --- /dev/null +++ b/tests/e2e_tests/test_set_subnet_identity_extrinsic.py @@ -0,0 +1,229 @@ +import pytest + +from bittensor.core.chain_data import SubnetIdentity +from bittensor.utils.btlogging import logging + + +def test_set_subnet_identity_extrinsic_happy_pass(subtensor, alice_wallet): + logging.console.info( + "Testing [blue]test_set_subnet_identity_extrinsic_happy_pass[/blue]" + ) + + netuid = subtensor.subnets.get_total_subnets() # 2 + + # Register a subnet, netuid 2 + assert subtensor.subnets.register_subnet(alice_wallet), "Subnet wasn't created" + + # Verify subnet created successfully + assert subtensor.subnets.subnet_exists(netuid), "Subnet wasn't created successfully" + + # Make sure subnet_identity is empty + assert subtensor.subnets.subnet(netuid).subnet_identity is None, ( + "Subnet identity should be None before set" + ) + + # Prepare SubnetIdentity for subnet + subnet_identity = SubnetIdentity( + subnet_name="e2e test subnet", + github_repo="e2e test repo", + subnet_contact="e2e test contact", + subnet_url="e2e test url", + logo_url="e2e test logo url", + discord="e2e test discord", + description="e2e test description", + additional="e2e test additional", + ) + + # Set SubnetIdentity to subnet + assert ( + subtensor.subnets.set_subnet_identity( + wallet=alice_wallet, + netuid=netuid, + subnet_identity=subnet_identity, + )[0] + is True + ), "Set subnet identity failed" + + # Check SubnetIdentity of the subnet + assert subtensor.subnets.subnet(netuid).subnet_identity == subnet_identity + + logging.console.success( + "✅ Passed [blue]test_set_subnet_identity_extrinsic_happy_pass[/blue]" + ) + + +@pytest.mark.asyncio +async def test_set_subnet_identity_extrinsic_happy_pass_async( + async_subtensor, alice_wallet +): + logging.console.info( + "Testing [blue]test_set_subnet_identity_extrinsic_happy_pass_async[/blue]" + ) + + netuid = await async_subtensor.subnets.get_total_subnets() # 2 + + # Register a subnet, netuid 2 + assert await async_subtensor.subnets.register_subnet(alice_wallet), ( + "Subnet wasn't created" + ) + + # Verify subnet created successfully + assert await async_subtensor.subnets.subnet_exists(netuid), ( + "Subnet wasn't created successfully" + ) + + # Make sure subnet_identity is empty + assert (await async_subtensor.subnets.subnet(netuid)).subnet_identity is None, ( + "Subnet identity should be None before set" + ) + + # Prepare SubnetIdentity for subnet + subnet_identity = SubnetIdentity( + subnet_name="e2e test subnet", + github_repo="e2e test repo", + subnet_contact="e2e test contact", + subnet_url="e2e test url", + logo_url="e2e test logo url", + discord="e2e test discord", + description="e2e test description", + additional="e2e test additional", + ) + + # Set SubnetIdentity to subnet + assert ( + await async_subtensor.subnets.set_subnet_identity( + wallet=alice_wallet, + netuid=netuid, + subnet_identity=subnet_identity, + ) + )[0] is True, "Set subnet identity failed" + + # Check SubnetIdentity of the subnet + assert ( + await async_subtensor.subnets.subnet(netuid) + ).subnet_identity == subnet_identity + logging.console.success( + "✅ Passed [blue]test_set_subnet_identity_extrinsic_happy_pass_async[/blue]" + ) + + +def test_set_subnet_identity_extrinsic_failed(subtensor, alice_wallet, bob_wallet): + """ + Test case for verifying the behavior of the `set_subnet_identity_extrinsic` function in the + scenario where the result of the function is expected to fail. It ensures proper handling + and validation when attempting to set the subnet identity under specific conditions. + + Args: + subtensor: The instance of the subtensor class under test. + alice_wallet: A mock or test wallet associated with Alice, used for creating a subnet. + bob_wallet: A mock or test wallet associated with Bob, used for setting the subnet identity. + + Decorators: + @pytest.mark.asyncio: Marks this test as an asynchronous test. + """ + logging.console.info( + "Testing [blue]test_set_subnet_identity_extrinsic_failed[/blue]" + ) + + netuid = subtensor.subnets.get_total_subnets() # 2 + + # Register a subnet, netuid 2 + assert subtensor.subnets.register_subnet(alice_wallet), "Subnet wasn't created" + + # Verify subnet created successfully + assert subtensor.subnets.subnet_exists(netuid), "Subnet wasn't created successfully" + + # Make sure subnet_identity is empty + assert subtensor.subnets.subnet(netuid).subnet_identity is None, ( + "Subnet identity should be None before set" + ) + + # Prepare SubnetIdentity for subnet + subnet_identity = SubnetIdentity( + subnet_name="e2e test subnet", + github_repo="e2e test repo", + subnet_contact="e2e test contact", + subnet_url="e2e test url", + logo_url="e2e test logo url", + discord="e2e test discord", + description="e2e test description", + additional="e2e test additional", + ) + + # Set SubnetIdentity to subnet with wrong wallet + assert ( + subtensor.subnets.set_subnet_identity( + wallet=bob_wallet, + netuid=netuid, + subnet_identity=subnet_identity, + )[0] + is False + ), "Set subnet identity failed" + + logging.console.success( + "✅ Passed [blue]test_set_subnet_identity_extrinsic_failed[/blue]" + ) + + +@pytest.mark.asyncio +async def test_set_subnet_identity_extrinsic_failed_async( + async_subtensor, alice_wallet, bob_wallet +): + """ + Async test case for verifying the behavior of the `set_subnet_identity_extrinsic` function in the + scenario where the result of the function is expected to fail. It ensures proper handling + and validation when attempting to set the subnet identity under specific conditions. + + Args: + subtensor: The instance of the subtensor class under test. + alice_wallet: A mock or test wallet associated with Alice, used for creating a subnet. + bob_wallet: A mock or test wallet associated with Bob, used for setting the subnet identity. + + Decorators: + @pytest.mark.asyncio: Marks this test as an asynchronous test. + """ + logging.console.info( + "Testing [blue]test_set_subnet_identity_extrinsic_failed[/blue]" + ) + + netuid = await async_subtensor.subnets.get_total_subnets() # 2 + + # Register a subnet, netuid 2 + assert await async_subtensor.subnets.register_subnet(alice_wallet), ( + "Subnet wasn't created" + ) + + # Verify subnet created successfully + assert await async_subtensor.subnets.subnet_exists(netuid), ( + "Subnet wasn't created successfully" + ) + + # Make sure subnet_identity is empty + assert (await async_subtensor.subnets.subnet(netuid)).subnet_identity is None, ( + "Subnet identity should be None before set" + ) + + # Prepare SubnetIdentity for subnet + subnet_identity = SubnetIdentity( + subnet_name="e2e test subnet", + github_repo="e2e test repo", + subnet_contact="e2e test contact", + subnet_url="e2e test url", + logo_url="e2e test logo url", + discord="e2e test discord", + description="e2e test description", + additional="e2e test additional", + ) + + # Set SubnetIdentity to subnet with wrong wallet + assert ( + await async_subtensor.subnets.set_subnet_identity( + wallet=bob_wallet, + netuid=netuid, + subnet_identity=subnet_identity, + ) + )[0] is False, "Set subnet identity failed" + + logging.console.success( + "✅ Passed [blue]test_set_subnet_identity_extrinsic_failed[/blue]" + ) diff --git a/tests/e2e_tests/test_set_weights.py b/tests/e2e_tests/test_set_weights.py new file mode 100644 index 0000000000..ea754b68c8 --- /dev/null +++ b/tests/e2e_tests/test_set_weights.py @@ -0,0 +1,398 @@ +import numpy as np +import pytest +import retry +import time +from bittensor.utils.balance import Balance +from bittensor.utils.btlogging import logging +from bittensor.utils.weight_utils import convert_weights_and_uids_for_emit +from tests.e2e_tests.utils.chain_interactions import ( + async_sudo_set_hyperparameter_bool, + async_sudo_set_admin_utils, + sudo_set_hyperparameter_bool, + sudo_set_admin_utils, + execute_and_wait_for_next_nonce, +) +from tests.e2e_tests.utils.e2e_test_utils import ( + async_wait_to_start_call, + wait_to_start_call, +) + + +def test_set_weights_uses_next_nonce(subtensor, alice_wallet): + """ + Tests that setting weights doesn't re-use a nonce in the transaction pool. + + Steps: + 1. Register three subnets through Alice + 2. Register Alice's neuron on each subnet and add stake + 3. Verify Alice has a vpermit on each subnet + 4. Lower the set weights rate limit on each subnet + 5. Set weights on each subnet + 6. Assert that all the set weights succeeded + Raises: + AssertionError: If any of the checks or verifications fail + """ + logging.console.info("Testing [blue]test_set_weights_uses_next_nonce[/blue]") + + netuids = [2, 3] + subnet_tempo = 50 + + # Lower the network registration rate limit and cost + sudo_set_admin_utils( + substrate=subtensor.substrate, + wallet=alice_wallet, + call_function="sudo_set_network_rate_limit", + call_params={"rate_limit": "0"}, # No limit + ) + # Set lock reduction interval + sudo_set_admin_utils( + substrate=subtensor.substrate, + wallet=alice_wallet, + call_function="sudo_set_lock_reduction_interval", + call_params={"interval": "1"}, # 1 block # reduce lock every block + ) + + for netuid in netuids: + # Register the subnets + assert subtensor.subnets.register_subnet(alice_wallet), ( + "Unable to register the subnet" + ) + + # Verify all subnets created successfully + assert subtensor.subnets.subnet_exists(netuid), ( + "Subnet wasn't created successfully" + ) + + # Weights sensitive to epoch changes + assert sudo_set_admin_utils( + substrate=subtensor.substrate, + wallet=alice_wallet, + call_function="sudo_set_tempo", + call_params={ + "netuid": netuid, + "tempo": subnet_tempo, + }, + ) + + assert wait_to_start_call(subtensor, alice_wallet, netuid) + + # Make sure 2 epochs are passed + subtensor.wait_for_block(subnet_tempo * 2 + 1) + + # Stake to become to top neuron after the first epoch + for netuid in netuids: + assert subtensor.staking.add_stake( + wallet=alice_wallet, + netuid=netuid, + hotkey_ss58=alice_wallet.hotkey.ss58_address, + amount=Balance.from_tao(10_000), + ) + + # Set weight hyperparameters per subnet + for netuid in netuids: + assert sudo_set_hyperparameter_bool( + substrate=subtensor.substrate, + wallet=alice_wallet, + call_function="sudo_set_commit_reveal_weights_enabled", + value=False, + netuid=netuid, + ), "Unable to enable commit reveal on the subnet" + + assert not subtensor.subnets.commit_reveal_enabled( + netuid, + ), "Failed to enable commit/reveal" + + assert subtensor.subnets.weights_rate_limit(netuid=netuid) > 0, ( + "Weights rate limit is below 0" + ) + + # Lower set weights rate limit + status, error = sudo_set_admin_utils( + substrate=subtensor.substrate, + wallet=alice_wallet, + call_function="sudo_set_weights_set_rate_limit", + call_params={"netuid": netuid, "weights_set_rate_limit": "0"}, + ) + + assert error is None + assert status is True + + assert ( + subtensor.subnets.get_subnet_hyperparameters( + netuid=netuid + ).weights_rate_limit + == 0 + ), "Failed to set weights_rate_limit" + assert subtensor.subnets.get_hyperparameter("WeightsSetRateLimit", netuid) == 0 + assert subtensor.subnets.weights_rate_limit(netuid=netuid) == 0 + + # Weights values + uids = np.array([0], dtype=np.int64) + weights = np.array([0.5], dtype=np.float32) + weight_uids, weight_vals = convert_weights_and_uids_for_emit( + uids=uids, weights=weights + ) + + logging.console.info( + f"[orange]Nonce before first set_weights: " + f"{subtensor.substrate.get_account_next_index(alice_wallet.hotkey.ss58_address)}[/orange]" + ) + + # 3 time doing call if nonce wasn't updated, then raise error + @retry.retry(exceptions=Exception, tries=3, delay=1) + @execute_and_wait_for_next_nonce(subtensor=subtensor, wallet=alice_wallet) + def set_weights(netuid_): + success, message = subtensor.extrinsics.set_weights( + wallet=alice_wallet, + netuid=netuid_, + uids=weight_uids, + weights=weight_vals, + wait_for_inclusion=True, + wait_for_finalization=False, + period=subnet_tempo, + ) + assert success is True, message + + logging.console.info( + f"[orange]Nonce after second set_weights: " + f"{subtensor.substrate.get_account_next_index(alice_wallet.hotkey.ss58_address)}[/orange]" + ) + + # Set weights for each subnet + for netuid in netuids: + set_weights(netuid) + + for netuid in netuids: + # Query the Weights storage map for all three subnets + query = subtensor.queries.query_module( + module="SubtensorModule", + name="Weights", + params=[netuid, 0], # Alice should be the only UID + ) + + weights = query.value + logging.console.info(f"Weights for subnet {netuid}: {weights}") + + assert weights is not None, f"Weights not found for subnet {netuid}" + assert weights == list(zip(weight_uids, weight_vals)), ( + f"Weights do not match for subnet {netuid}" + ) + + logging.console.info("✅ Passed [blue]test_set_weights_uses_next_nonce[/blue]") + + +@pytest.mark.asyncio +async def test_set_weights_uses_next_nonce_async(async_subtensor, alice_wallet): + """ + Async tests that setting weights doesn't re-use a nonce in the transaction pool. + + Steps: + 1. Register three subnets through Alice + 2. Register Alice's neuron on each subnet and add stake + 3. Verify Alice has a vpermit on each subnet + 4. Lower the set weights rate limit on each subnet + 5. Set weights on each subnet + 6. Assert that all the set weights succeeded + Raises: + AssertionError: If any of the checks or verifications fail + """ + logging.console.info("Testing [blue]test_set_weights_uses_next_nonce_async[/blue]") + + netuids = [2, 3] + subnet_tempo = 50 + + # Lower the network registration rate limit and cost + await async_sudo_set_admin_utils( + substrate=async_subtensor.substrate, + wallet=alice_wallet, + call_function="sudo_set_network_rate_limit", + call_params={"rate_limit": "0"}, # No limit + ) + # Set lock reduction interval + await async_sudo_set_admin_utils( + substrate=async_subtensor.substrate, + wallet=alice_wallet, + call_function="sudo_set_lock_reduction_interval", + call_params={"interval": "1"}, # 1 block # reduce lock every block + ) + + for netuid in netuids: + # Register the subnets + assert await async_subtensor.subnets.register_subnet(alice_wallet), ( + "Unable to register the subnet" + ) + + # Verify all subnets created successfully + assert await async_subtensor.subnets.subnet_exists(netuid), ( + "Subnet wasn't created successfully" + ) + + # Weights sensitive to epoch changes + assert await async_sudo_set_admin_utils( + substrate=async_subtensor.substrate, + wallet=alice_wallet, + call_function="sudo_set_tempo", + call_params={ + "netuid": netuid, + "tempo": subnet_tempo, + }, + ) + + assert await async_wait_to_start_call(async_subtensor, alice_wallet, netuid) + + # Make sure 2 epochs are passed + await async_subtensor.wait_for_block(subnet_tempo * 2 + 1) + + # Stake to become to top neuron after the first epoch + for netuid in netuids: + assert await async_subtensor.staking.add_stake( + wallet=alice_wallet, + netuid=netuid, + hotkey_ss58=alice_wallet.hotkey.ss58_address, + amount=Balance.from_tao(10_000), + ) + + # Set weight hyperparameters per subnet + for netuid in netuids: + assert await async_sudo_set_hyperparameter_bool( + substrate=async_subtensor.substrate, + wallet=alice_wallet, + call_function="sudo_set_commit_reveal_weights_enabled", + value=False, + netuid=netuid, + ), "Unable to enable commit reveal on the subnet" + + assert not await async_subtensor.subnets.commit_reveal_enabled( + netuid, + ), "Failed to enable commit/reveal" + + assert await async_subtensor.subnets.weights_rate_limit(netuid=netuid) > 0, ( + "Weights rate limit is below 0" + ) + + # Lower set weights rate limit + status, error = await async_sudo_set_admin_utils( + substrate=async_subtensor.substrate, + wallet=alice_wallet, + call_function="sudo_set_weights_set_rate_limit", + call_params={"netuid": netuid, "weights_set_rate_limit": "0"}, + ) + + assert error is None + assert status is True + + assert ( + await async_subtensor.subnets.get_subnet_hyperparameters(netuid=netuid) + ).weights_rate_limit == 0, "Failed to set weights_rate_limit" + assert ( + await async_subtensor.subnets.get_hyperparameter( + "WeightsSetRateLimit", netuid + ) + == 0 + ) + assert await async_subtensor.subnets.weights_rate_limit(netuid=netuid) == 0 + + # Weights values + uids = np.array([0], dtype=np.int64) + weights = np.array([0.5], dtype=np.float32) + weight_uids, weight_vals = convert_weights_and_uids_for_emit( + uids=uids, weights=weights + ) + + logging.console.info( + f"[orange]Nonce before first set_weights: " + f"{await async_subtensor.substrate.get_account_nonce(alice_wallet.hotkey.ss58_address)}[/orange]" + ) + + # # 3 time doing call if nonce wasn't updated, then raise error + # @retry.retry(exceptions=Exception, tries=3, delay=1) + # @execute_and_wait_for_next_nonce(subtensor=async_subtensor, wallet=alice_wallet) + # def set_weights(netuid_): + # success, message = subtensor.extrinsics.set_weights( + # wallet=alice_wallet, + # netuid=netuid_, + # uids=weight_uids, + # weights=weight_vals, + # wait_for_inclusion=True, + # wait_for_finalization=False, + # period=subnet_tempo, + # ) + # assert success is True, message + + async def set_weights(netuid_): + """ + To avoid adding asynchronous retrieval to dependencies, we implement a retrieval behavior with asynchronous + behavior. + """ + + async def set_weights_(): + success_, message_ = await async_subtensor.extrinsics.set_weights( + wallet=alice_wallet, + netuid=netuid_, + uids=weight_uids, + weights=weight_vals, + wait_for_inclusion=True, + wait_for_finalization=False, + period=subnet_tempo, + ) + assert success_ is True, message_ + + max_retries = 3 + timeout = 60.0 + sleep = 0.25 if async_subtensor.chain.is_fast_blocks() else 12.0 + + for attempt in range(1, max_retries + 1): + try: + start_nonce = await async_subtensor.substrate.get_account_nonce( + alice_wallet.hotkey.ss58_address + ) + + result = await set_weights_() + + start = time.time() + while (time.time() - start) < timeout: + current_nonce = await async_subtensor.substrate.get_account_nonce( + alice_wallet.hotkey.ss58_address + ) + + if current_nonce != start_nonce: + logging.console.info( + f"✅ Nonce changed from {start_nonce} to {current_nonce}" + ) + return result + logging.console.info( + f"⏳ Waiting for nonce increment. Current: {current_nonce}" + ) + time.sleep(sleep) + except Exception as e: + raise e + raise Exception(f"Failed to commit weights after {max_retries} attempts.") + + # Set weights for each subnet + for netuid in netuids: + await set_weights(netuid) + + logging.console.info( + f"[orange]Nonce after second set_weights: " + f"{await async_subtensor.substrate.get_account_nonce(alice_wallet.hotkey.ss58_address)}[/orange]" + ) + + for netuid in netuids: + # Query the Weights storage map for all three subnets + query = await async_subtensor.queries.query_module( + module="SubtensorModule", + name="Weights", + params=[netuid, 0], # Alice should be the only UID + ) + + weights = query.value + logging.console.info(f"Weights for subnet {netuid}: {weights}") + + assert weights is not None, f"Weights not found for subnet {netuid}" + assert weights == list(zip(weight_uids, weight_vals)), ( + f"Weights do not match for subnet {netuid}" + ) + + logging.console.info( + "✅ Passed [blue]test_set_weights_uses_next_nonce_async[/blue]" + ) diff --git a/tests/e2e_tests/test_stake_fee.py b/tests/e2e_tests/test_stake_fee.py new file mode 100644 index 0000000000..8d26ecbed2 --- /dev/null +++ b/tests/e2e_tests/test_stake_fee.py @@ -0,0 +1,274 @@ +import pytest +from bittensor.utils.balance import Balance +from bittensor.utils.btlogging import logging + + +def test_stake_fee_api(subtensor, alice_wallet, bob_wallet): + """ + Tests the stake fee calculation mechanism for various staking operations + + Steps: + 1. Register a subnet through Alice + 2. Test stake fees for: + - Adding new stake + - Removing stake + - Moving stake between hotkeys/subnets/coldkeys + """ + logging.console.info("Testing [blue]test_stake_fee_api[/blue]") + + netuid = 2 + root_netuid = 0 + stake_amount = Balance.from_tao(100) # 100 TAO + min_stake_fee = Balance.from_tao(0.050354772) + + # Register subnet as Alice + assert subtensor.subnets.register_subnet(alice_wallet), ( + "Unable to register the subnet" + ) + assert subtensor.subnets.subnet_exists(netuid), "Subnet wasn't created successfully" + + # Test add_stake fee + stake_fee_0 = subtensor.staking.get_stake_add_fee( + amount=stake_amount, + netuid=netuid, + coldkey_ss58=alice_wallet.coldkeypub.ss58_address, + hotkey_ss58=alice_wallet.hotkey.ss58_address, + ) + assert isinstance(stake_fee_0, Balance), "Stake fee should be a Balance object." + assert stake_fee_0 == min_stake_fee, ( + "Stake fee should be equal the minimum stake fee." + ) + + # Test unstake fee + unstake_fee_root = subtensor.staking.get_unstake_fee( + amount=stake_amount, + netuid=root_netuid, + coldkey_ss58=alice_wallet.coldkeypub.ss58_address, + hotkey_ss58=bob_wallet.hotkey.ss58_address, + ) + assert isinstance(unstake_fee_root, Balance), ( + "Stake fee should be a Balance object." + ) + assert unstake_fee_root == min_stake_fee, ( + "Root unstake fee should be equal the minimum stake fee." + ) + + # Test various stake movement scenarios + movement_scenarios = [ + # Move from root to non-root + { + "origin_netuid": root_netuid, + "origin_hotkey": alice_wallet.hotkey.ss58_address, + "origin_coldkey": alice_wallet.coldkeypub.ss58_address, + "dest_netuid": netuid, + "dest_hotkey": alice_wallet.hotkey.ss58_address, + "dest_coldkey": alice_wallet.coldkeypub.ss58_address, + "stake_fee": min_stake_fee, + }, + # Move between hotkeys on root + { + "origin_netuid": root_netuid, + "origin_hotkey": alice_wallet.hotkey.ss58_address, + "origin_coldkey": alice_wallet.coldkeypub.ss58_address, + "dest_netuid": root_netuid, + "dest_hotkey": bob_wallet.hotkey.ss58_address, + "dest_coldkey": alice_wallet.coldkeypub.ss58_address, + "stake_fee": 0, + }, + # Move between coldkeys on root + { + "origin_netuid": root_netuid, + "origin_hotkey": bob_wallet.hotkey.ss58_address, + "origin_coldkey": alice_wallet.coldkeypub.ss58_address, + "dest_netuid": root_netuid, + "dest_hotkey": bob_wallet.hotkey.ss58_address, + "dest_coldkey": bob_wallet.coldkeypub.ss58_address, + "stake_fee": 0, + }, + # Move between coldkeys on non-root + { + "origin_netuid": netuid, + "origin_hotkey": bob_wallet.hotkey.ss58_address, + "origin_coldkey": alice_wallet.coldkeypub.ss58_address, + "dest_netuid": netuid, + "dest_hotkey": bob_wallet.hotkey.ss58_address, + "dest_coldkey": bob_wallet.coldkeypub.ss58_address, + "stake_fee": min_stake_fee, + }, + ] + + for scenario in movement_scenarios: + stake_fee = subtensor.staking.get_stake_movement_fee( + amount=stake_amount, + origin_netuid=scenario["origin_netuid"], + origin_hotkey_ss58=scenario["origin_hotkey"], + origin_coldkey_ss58=scenario["origin_coldkey"], + destination_netuid=scenario["dest_netuid"], + destination_hotkey_ss58=scenario["dest_hotkey"], + destination_coldkey_ss58=scenario["dest_coldkey"], + ) + assert isinstance(stake_fee, Balance), "Stake fee should be a Balance object" + assert stake_fee >= scenario["stake_fee"], ( + "Stake fee should be greater than the minimum stake fee" + ) + + # Test cross-subnet movement + netuid2 = 3 + assert subtensor.subnets.register_subnet(alice_wallet), ( + "Unable to register the second subnet" + ) + assert subtensor.subnets.subnet_exists(netuid2), ( + "Second subnet wasn't created successfully" + ) + + stake_fee = subtensor.staking.get_stake_movement_fee( + amount=stake_amount, + origin_netuid=netuid, + origin_hotkey_ss58=bob_wallet.hotkey.ss58_address, + origin_coldkey_ss58=alice_wallet.coldkeypub.ss58_address, + destination_netuid=netuid2, + destination_hotkey_ss58=bob_wallet.hotkey.ss58_address, + destination_coldkey_ss58=alice_wallet.coldkeypub.ss58_address, + ) + assert isinstance(stake_fee, Balance), "Stake fee should be a Balance object" + assert stake_fee >= min_stake_fee, ( + "Stake fee should be greater than the minimum stake fee" + ) + logging.console.success("✅ Passed [blue]test_stake_fee_api[/blue]") + + +@pytest.mark.asyncio +async def test_stake_fee_api_async(async_subtensor, alice_wallet, bob_wallet): + """ + Tests the stake fee calculation mechanism for various staking operations + + Steps: + 1. Register a subnet through Alice + 2. Test stake fees for: + - Adding new stake + - Removing stake + - Moving stake between hotkeys/subnets/coldkeys + """ + logging.console.info("Testing [blue]test_stake_fee_api_async[/blue]") + + netuid = 2 + root_netuid = 0 + stake_amount = Balance.from_tao(100) # 100 TAO + min_stake_fee = Balance.from_tao(0.050354772) + + # Register subnet as Alice + assert await async_subtensor.subnets.register_subnet(alice_wallet), ( + "Unable to register the subnet" + ) + assert await async_subtensor.subnets.subnet_exists(netuid), ( + "Subnet wasn't created successfully" + ) + + # Test add_stake fee + stake_fee_0 = await async_subtensor.staking.get_stake_add_fee( + amount=stake_amount, + netuid=netuid, + coldkey_ss58=alice_wallet.coldkeypub.ss58_address, + hotkey_ss58=alice_wallet.hotkey.ss58_address, + ) + assert isinstance(stake_fee_0, Balance), "Stake fee should be a Balance object." + assert stake_fee_0 == min_stake_fee, ( + "Stake fee should be equal the minimum stake fee." + ) + + # Test unstake fee + unstake_fee_root = await async_subtensor.staking.get_unstake_fee( + amount=stake_amount, + netuid=root_netuid, + coldkey_ss58=alice_wallet.coldkeypub.ss58_address, + hotkey_ss58=bob_wallet.hotkey.ss58_address, + ) + assert isinstance(unstake_fee_root, Balance), ( + "Stake fee should be a Balance object." + ) + assert unstake_fee_root == min_stake_fee, ( + "Root unstake fee should be equal the minimum stake fee." + ) + + # Test various stake movement scenarios + movement_scenarios = [ + # Move from root to non-root + { + "origin_netuid": root_netuid, + "origin_hotkey": alice_wallet.hotkey.ss58_address, + "origin_coldkey": alice_wallet.coldkeypub.ss58_address, + "dest_netuid": netuid, + "dest_hotkey": alice_wallet.hotkey.ss58_address, + "dest_coldkey": alice_wallet.coldkeypub.ss58_address, + "stake_fee": min_stake_fee, + }, + # Move between hotkeys on root + { + "origin_netuid": root_netuid, + "origin_hotkey": alice_wallet.hotkey.ss58_address, + "origin_coldkey": alice_wallet.coldkeypub.ss58_address, + "dest_netuid": root_netuid, + "dest_hotkey": bob_wallet.hotkey.ss58_address, + "dest_coldkey": alice_wallet.coldkeypub.ss58_address, + "stake_fee": 0, + }, + # Move between coldkeys on root + { + "origin_netuid": root_netuid, + "origin_hotkey": bob_wallet.hotkey.ss58_address, + "origin_coldkey": alice_wallet.coldkeypub.ss58_address, + "dest_netuid": root_netuid, + "dest_hotkey": bob_wallet.hotkey.ss58_address, + "dest_coldkey": bob_wallet.coldkeypub.ss58_address, + "stake_fee": 0, + }, + # Move between coldkeys on non-root + { + "origin_netuid": netuid, + "origin_hotkey": bob_wallet.hotkey.ss58_address, + "origin_coldkey": alice_wallet.coldkeypub.ss58_address, + "dest_netuid": netuid, + "dest_hotkey": bob_wallet.hotkey.ss58_address, + "dest_coldkey": bob_wallet.coldkeypub.ss58_address, + "stake_fee": min_stake_fee, + }, + ] + + for scenario in movement_scenarios: + stake_fee = await async_subtensor.staking.get_stake_movement_fee( + amount=stake_amount, + origin_netuid=scenario["origin_netuid"], + origin_hotkey_ss58=scenario["origin_hotkey"], + origin_coldkey_ss58=scenario["origin_coldkey"], + destination_netuid=scenario["dest_netuid"], + destination_hotkey_ss58=scenario["dest_hotkey"], + destination_coldkey_ss58=scenario["dest_coldkey"], + ) + assert isinstance(stake_fee, Balance), "Stake fee should be a Balance object" + assert stake_fee >= scenario["stake_fee"], ( + "Stake fee should be greater than the minimum stake fee" + ) + + # Test cross-subnet movement + netuid2 = 3 + assert await async_subtensor.subnets.register_subnet(alice_wallet), ( + "Unable to register the second subnet" + ) + assert await async_subtensor.subnets.subnet_exists(netuid2), ( + "Second subnet wasn't created successfully" + ) + + stake_fee = await async_subtensor.staking.get_stake_movement_fee( + amount=stake_amount, + origin_netuid=netuid, + origin_hotkey_ss58=bob_wallet.hotkey.ss58_address, + origin_coldkey_ss58=alice_wallet.coldkeypub.ss58_address, + destination_netuid=netuid2, + destination_hotkey_ss58=bob_wallet.hotkey.ss58_address, + destination_coldkey_ss58=alice_wallet.coldkeypub.ss58_address, + ) + assert isinstance(stake_fee, Balance), "Stake fee should be a Balance object" + assert stake_fee >= min_stake_fee, ( + "Stake fee should be greater than the minimum stake fee" + ) + logging.console.success("✅ Passed [blue]test_stake_fee_api_async[/blue]") diff --git a/tests/e2e_tests/test_staking.py b/tests/e2e_tests/test_staking.py new file mode 100644 index 0000000000..b3847bb507 --- /dev/null +++ b/tests/e2e_tests/test_staking.py @@ -0,0 +1,2075 @@ +import pytest +import asyncio + +from bittensor import logging +from bittensor.core.chain_data.stake_info import StakeInfo +from bittensor.core.errors import ChainError +from bittensor.utils.balance import Balance +from tests.e2e_tests.utils.chain_interactions import ( + async_sudo_set_admin_utils, + get_dynamic_balance, + sudo_set_admin_utils, +) +from tests.e2e_tests.utils.e2e_test_utils import ( + async_wait_to_start_call, + wait_to_start_call, +) +from tests.helpers.helpers import CloseInValue + + +def test_single_operation(subtensor, alice_wallet, bob_wallet): + """ + Tests: + - Staking using `add_stake` + - Unstaking using `unstake` + - Checks StakeInfo + """ + logging.console.info("Testing [blue]test_single_operation[/blue]") + + alice_subnet_netuid = subtensor.subnets.get_total_subnets() # 2 + + # Register root as Alice - the subnet owner and validator + assert subtensor.subnets.register_subnet(alice_wallet) + + # Verify subnet created successfully + assert subtensor.subnets.subnet_exists(alice_subnet_netuid), ( + "Subnet wasn't created successfully" + ) + + assert wait_to_start_call(subtensor, alice_wallet, alice_subnet_netuid) + + subtensor.subnets.burned_register( + wallet=alice_wallet, + netuid=alice_subnet_netuid, + ) + logging.console.success(f"Alice is registered in subnet {alice_subnet_netuid}") + subtensor.subnets.burned_register( + wallet=bob_wallet, + netuid=alice_subnet_netuid, + ) + logging.console.success(f"Bob is registered in subnet {alice_subnet_netuid}") + + stake = subtensor.staking.get_stake( + coldkey_ss58=alice_wallet.coldkey.ss58_address, + hotkey_ss58=bob_wallet.hotkey.ss58_address, + netuid=alice_subnet_netuid, + ) + + assert stake == Balance(0).set_unit(alice_subnet_netuid) + + success = subtensor.staking.add_stake( + wallet=alice_wallet, + netuid=alice_subnet_netuid, + hotkey_ss58=bob_wallet.hotkey.ss58_address, + amount=Balance.from_tao(1), + period=16, + ) + + assert success is True + + stake_alice = subtensor.staking.get_stake( + coldkey_ss58=alice_wallet.coldkey.ss58_address, + hotkey_ss58=alice_wallet.hotkey.ss58_address, + netuid=alice_subnet_netuid, + ) + logging.console.info(f"Alice stake: {stake_alice}") + + stake_bob = subtensor.staking.get_stake( + coldkey_ss58=alice_wallet.coldkey.ss58_address, + hotkey_ss58=bob_wallet.hotkey.ss58_address, + netuid=alice_subnet_netuid, + ) + + logging.console.info(f"Bob stake: {stake_bob}") + assert stake_bob > Balance(0).set_unit(alice_subnet_netuid) + + stakes = subtensor.staking.get_stake_for_coldkey(alice_wallet.coldkey.ss58_address) + + expected_stakes = [ + StakeInfo( + hotkey_ss58=stakes[0].hotkey_ss58, + coldkey_ss58=alice_wallet.coldkey.ss58_address, + netuid=alice_subnet_netuid, + stake=get_dynamic_balance(stakes[0].stake.rao, alice_subnet_netuid), + locked=Balance(0).set_unit(alice_subnet_netuid), + emission=get_dynamic_balance(stakes[0].emission.rao, alice_subnet_netuid), + drain=0, + is_registered=True, + ), + ] + + fast_blocks_stake = ( + [ + StakeInfo( + hotkey_ss58=stakes[1].hotkey_ss58, + coldkey_ss58=alice_wallet.coldkey.ss58_address, + netuid=alice_subnet_netuid, + stake=get_dynamic_balance(stakes[1].stake.rao, alice_subnet_netuid), + locked=Balance(0).set_unit(alice_subnet_netuid), + emission=get_dynamic_balance( + stakes[1].emission.rao, alice_subnet_netuid + ), + drain=0, + is_registered=True, + ) + ] + if subtensor.chain.is_fast_blocks() + else [] + ) + + expected_stakes += fast_blocks_stake + + assert stakes == expected_stakes + assert ( + subtensor.staking.get_stake_for_coldkey + == subtensor.staking.get_stake_info_for_coldkey + ) + + stakes = subtensor.staking.get_stake_for_coldkey_and_hotkey( + alice_wallet.coldkey.ss58_address, + bob_wallet.hotkey.ss58_address, + ) + + assert stakes == { + 0: StakeInfo( + hotkey_ss58=bob_wallet.hotkey.ss58_address, + coldkey_ss58=alice_wallet.coldkey.ss58_address, + netuid=0, + stake=Balance(0), + locked=Balance(0), + emission=Balance(0), + drain=0, + is_registered=False, + ), + 1: StakeInfo( + hotkey_ss58=bob_wallet.hotkey.ss58_address, + coldkey_ss58=alice_wallet.coldkey.ss58_address, + netuid=1, + stake=stake.set_unit(1), + locked=Balance.from_tao(0, netuid=1), + emission=Balance.from_tao(0, netuid=1), + drain=0, + is_registered=False, + ), + 2: StakeInfo( + hotkey_ss58=bob_wallet.hotkey.ss58_address, + coldkey_ss58=alice_wallet.coldkey.ss58_address, + netuid=alice_subnet_netuid, + stake=get_dynamic_balance(stakes[2].stake.rao, alice_subnet_netuid), + locked=Balance.from_tao(0, netuid=alice_subnet_netuid), + emission=get_dynamic_balance(stakes[2].emission.rao, alice_subnet_netuid), + drain=0, + is_registered=True, + ), + } + + stake = subtensor.staking.get_stake( + coldkey_ss58=alice_wallet.coldkey.ss58_address, + hotkey_ss58=bob_wallet.hotkey.ss58_address, + netuid=alice_subnet_netuid, + ) + logging.console.info(f"Alice stake before unstake: {stake}") + + # unstale all to check in later + success = subtensor.staking.unstake( + wallet=alice_wallet, + netuid=alice_subnet_netuid, + hotkey_ss58=bob_wallet.hotkey.ss58_address, + amount=stake, + period=16, + ) + + assert success is True + + stake = subtensor.staking.get_stake( + coldkey_ss58=alice_wallet.coldkey.ss58_address, + hotkey_ss58=bob_wallet.hotkey.ss58_address, + netuid=alice_subnet_netuid, + ) + + # all balances have been unstaked + assert stake == Balance(0).set_unit(alice_subnet_netuid) + logging.console.success("✅ Test [green]test_single_operation[/green] passed") + + +@pytest.mark.asyncio +async def test_single_operation_async(async_subtensor, alice_wallet, bob_wallet): + """ + Async ests: + - Staking using `add_stake` + - Unstaking using `unstake` + - Checks StakeInfo + """ + logging.console.info("Testing [blue]test_single_operation_async[/blue]") + + alice_subnet_netuid = await async_subtensor.subnets.get_total_subnets() # 2 + + # Register root as Alice - the subnet owner and validator + assert await async_subtensor.subnets.register_subnet(alice_wallet) + + # Verify subnet created successfully + assert await async_subtensor.subnets.subnet_exists(alice_subnet_netuid), ( + "Subnet wasn't created successfully" + ) + + assert await async_wait_to_start_call( + async_subtensor, alice_wallet, alice_subnet_netuid + ) + + await async_subtensor.subnets.burned_register( + wallet=alice_wallet, + netuid=alice_subnet_netuid, + ) + logging.console.success(f"Alice is registered in subnet {alice_subnet_netuid}") + await async_subtensor.subnets.burned_register( + wallet=bob_wallet, + netuid=alice_subnet_netuid, + ) + logging.console.success(f"Bob is registered in subnet {alice_subnet_netuid}") + + stake = await async_subtensor.staking.get_stake( + coldkey_ss58=alice_wallet.coldkey.ss58_address, + hotkey_ss58=bob_wallet.hotkey.ss58_address, + netuid=alice_subnet_netuid, + ) + + assert stake == Balance(0).set_unit(alice_subnet_netuid) + + success = await async_subtensor.staking.add_stake( + wallet=alice_wallet, + netuid=alice_subnet_netuid, + hotkey_ss58=bob_wallet.hotkey.ss58_address, + amount=Balance.from_tao(1), + period=16, + ) + + assert success is True + + stake_alice = await async_subtensor.staking.get_stake( + coldkey_ss58=alice_wallet.coldkey.ss58_address, + hotkey_ss58=alice_wallet.hotkey.ss58_address, + netuid=alice_subnet_netuid, + ) + logging.console.info(f"Alice stake: {stake_alice}") + + stake_bob = await async_subtensor.staking.get_stake( + coldkey_ss58=alice_wallet.coldkey.ss58_address, + hotkey_ss58=bob_wallet.hotkey.ss58_address, + netuid=alice_subnet_netuid, + ) + + logging.console.info(f"Bob stake: {stake_bob}") + assert stake_bob > Balance(0).set_unit(alice_subnet_netuid) + + stakes = await async_subtensor.staking.get_stake_for_coldkey( + alice_wallet.coldkey.ss58_address + ) + + expected_stakes = [ + StakeInfo( + hotkey_ss58=stakes[0].hotkey_ss58, + coldkey_ss58=alice_wallet.coldkey.ss58_address, + netuid=alice_subnet_netuid, + stake=get_dynamic_balance(stakes[0].stake.rao, alice_subnet_netuid), + locked=Balance(0).set_unit(alice_subnet_netuid), + emission=get_dynamic_balance(stakes[0].emission.rao, alice_subnet_netuid), + drain=0, + is_registered=True, + ), + ] + + fast_blocks_stake = ( + [ + StakeInfo( + hotkey_ss58=stakes[1].hotkey_ss58, + coldkey_ss58=alice_wallet.coldkey.ss58_address, + netuid=alice_subnet_netuid, + stake=get_dynamic_balance(stakes[1].stake.rao, alice_subnet_netuid), + locked=Balance(0).set_unit(alice_subnet_netuid), + emission=get_dynamic_balance( + stakes[1].emission.rao, alice_subnet_netuid + ), + drain=0, + is_registered=True, + ) + ] + if await async_subtensor.chain.is_fast_blocks() + else [] + ) + + expected_stakes += fast_blocks_stake + + assert stakes == expected_stakes + assert ( + async_subtensor.staking.get_stake_for_coldkey + == async_subtensor.staking.get_stake_info_for_coldkey + ) + + stakes = await async_subtensor.staking.get_stake_for_coldkey_and_hotkey( + alice_wallet.coldkey.ss58_address, + bob_wallet.hotkey.ss58_address, + ) + + assert stakes == { + 0: StakeInfo( + hotkey_ss58=bob_wallet.hotkey.ss58_address, + coldkey_ss58=alice_wallet.coldkey.ss58_address, + netuid=0, + stake=Balance(0), + locked=Balance(0), + emission=Balance(0), + drain=0, + is_registered=False, + ), + 1: StakeInfo( + hotkey_ss58=bob_wallet.hotkey.ss58_address, + coldkey_ss58=alice_wallet.coldkey.ss58_address, + netuid=1, + stake=stake.set_unit(1), + locked=Balance.from_tao(0, netuid=1), + emission=Balance.from_tao(0, netuid=1), + drain=0, + is_registered=False, + ), + 2: StakeInfo( + hotkey_ss58=bob_wallet.hotkey.ss58_address, + coldkey_ss58=alice_wallet.coldkey.ss58_address, + netuid=alice_subnet_netuid, + stake=get_dynamic_balance(stakes[2].stake.rao, alice_subnet_netuid), + locked=Balance.from_tao(0, netuid=alice_subnet_netuid), + emission=get_dynamic_balance(stakes[2].emission.rao, alice_subnet_netuid), + drain=0, + is_registered=True, + ), + } + + stake = await async_subtensor.staking.get_stake( + coldkey_ss58=alice_wallet.coldkey.ss58_address, + hotkey_ss58=bob_wallet.hotkey.ss58_address, + netuid=alice_subnet_netuid, + ) + logging.console.info(f"Alice stake before unstake: {stake}") + + # unstale all to check in later + success, message = await async_subtensor.staking.unstake_all( + wallet=alice_wallet, + netuid=alice_subnet_netuid, + hotkey=bob_wallet.hotkey.ss58_address, + period=16, + ) + assert success is True, message + + stake = await async_subtensor.staking.get_stake( + coldkey_ss58=alice_wallet.coldkey.ss58_address, + hotkey_ss58=bob_wallet.hotkey.ss58_address, + netuid=alice_subnet_netuid, + ) + logging.console.info(f"Alice stake after unstake: {stake}") + + # all balances have been unstaked + assert stake == Balance(0).set_unit(alice_subnet_netuid) + + logging.console.success("✅ Test [green]test_single_operation_async[/green] passed") + + +def test_batch_operations(subtensor, alice_wallet, bob_wallet): + """ + Tests: + - Staking using `add_stake_multiple` + - Unstaking using `unstake_multiple` + - Checks StakeInfo + - Checks Accounts Balance + """ + logging.console.info("Testing [blue]test_batch_operations[/blue]") + + netuids = [ + 2, + 3, + ] + + for _ in netuids: + subtensor.subnets.register_subnet(alice_wallet) + + # make sure we passed start_call limit for both subnets + for netuid in netuids: + assert wait_to_start_call(subtensor, alice_wallet, netuid) + + for netuid in netuids: + subtensor.subnets.burned_register( + wallet=bob_wallet, + netuid=netuid, + ) + + for netuid in netuids: + stake = subtensor.staking.get_stake( + alice_wallet.coldkey.ss58_address, + bob_wallet.hotkey.ss58_address, + netuid=netuid, + ) + + assert stake == Balance(0).set_unit(netuid), f"netuid={netuid} stake={stake}" + + balances = subtensor.wallets.get_balances( + alice_wallet.coldkey.ss58_address, + bob_wallet.coldkey.ss58_address, + ) + + expected_balances = { + alice_wallet.coldkey.ss58_address: get_dynamic_balance( + balances[alice_wallet.coldkey.ss58_address].rao + ), + bob_wallet.coldkey.ss58_address: get_dynamic_balance( + balances[bob_wallet.coldkey.ss58_address].rao + ), + } + + assert balances == expected_balances + + alice_balance = balances[alice_wallet.coldkey.ss58_address] + + success = subtensor.staking.add_stake_multiple( + wallet=alice_wallet, + hotkey_ss58s=[bob_wallet.hotkey.ss58_address for _ in netuids], + netuids=netuids, + amounts=[Balance.from_tao(10_000) for _ in netuids], + ) + + assert success is True + + stakes = [ + subtensor.staking.get_stake( + coldkey_ss58=alice_wallet.coldkey.ss58_address, + hotkey_ss58=bob_wallet.hotkey.ss58_address, + netuid=netuid, + ) + for netuid in netuids + ] + + for netuid, stake in zip(netuids, stakes): + assert stake > Balance(0).set_unit(netuid), f"netuid={netuid} stake={stake}" + + alice_balance -= len(netuids) * Balance.from_tao(10_000) + + balances = subtensor.wallets.get_balances( + alice_wallet.coldkey.ss58_address, + bob_wallet.coldkey.ss58_address, + ) + + expected_balances = { + alice_wallet.coldkey.ss58_address: get_dynamic_balance( + balances[alice_wallet.coldkey.ss58_address].rao + ), + bob_wallet.coldkey.ss58_address: get_dynamic_balance( + balances[bob_wallet.coldkey.ss58_address].rao + ), + } + + assert balances == expected_balances + + expected_fee_paid = Balance(0) + for netuid in netuids: + call = subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="remove_stake", + call_params={ + "hotkey": bob_wallet.hotkey.ss58_address, + "amount_unstaked": Balance.from_tao(100).rao, + "netuid": netuid, + }, + ) + payment_info = subtensor.substrate.get_payment_info( + call, alice_wallet.coldkeypub + ) + fee_alpha = Balance.from_rao(payment_info["partial_fee"]).set_unit(netuid) + dynamic_info = subtensor.subnets.subnet(netuid) + fee_tao = dynamic_info.alpha_to_tao(fee_alpha) + expected_fee_paid += fee_tao + + success = subtensor.staking.unstake_multiple( + wallet=alice_wallet, + netuids=netuids, + hotkey_ss58s=[bob_wallet.hotkey.ss58_address for _ in netuids], + amounts=[Balance.from_tao(100) for _ in netuids], + ) + + assert success is True + + for netuid, old_stake in zip(netuids, stakes): + stake = subtensor.staking.get_stake( + coldkey_ss58=alice_wallet.coldkey.ss58_address, + hotkey_ss58=bob_wallet.hotkey.ss58_address, + netuid=netuid, + ) + + assert stake < old_stake, f"netuid={netuid} stake={stake}" + + balances = subtensor.wallets.get_balances( + alice_wallet.coldkey.ss58_address, + bob_wallet.coldkey.ss58_address, + ) + + assert CloseInValue( # Make sure we are within 0.0001 TAO due to tx fees + balances[bob_wallet.coldkey.ss58_address], Balance.from_rao(100_000) + ) == Balance.from_tao(999_999.7994) + + assert balances[alice_wallet.coldkey.ss58_address] > alice_balance + logging.console.success("✅ Test [green]test_batch_operations[/green] passed") + + +@pytest.mark.asyncio +async def test_batch_operations_async(async_subtensor, alice_wallet, bob_wallet): + """ + Async tests: + - Staking using `add_stake_multiple` + - Unstaking using `unstake_multiple` + - Checks StakeInfo + - Checks Accounts Balance + """ + logging.console.info("Testing [blue]test_batch_operations_async[/blue]") + + netuids = [ + 2, + 3, + ] + + for _ in netuids: + await async_subtensor.subnets.register_subnet(alice_wallet) + + # make sure we passed start_call limit for both subnets + for netuid in netuids: + assert await async_wait_to_start_call(async_subtensor, alice_wallet, netuid) + + for netuid in netuids: + await async_subtensor.subnets.burned_register( + wallet=bob_wallet, + netuid=netuid, + ) + + for netuid in netuids: + stake = await async_subtensor.staking.get_stake( + coldkey_ss58=alice_wallet.coldkey.ss58_address, + hotkey_ss58=bob_wallet.hotkey.ss58_address, + netuid=netuid, + ) + + assert stake == Balance(0).set_unit(netuid), f"netuid={netuid} stake={stake}" + + balances = await async_subtensor.wallets.get_balances( + alice_wallet.coldkey.ss58_address, + bob_wallet.coldkey.ss58_address, + ) + + expected_balances = { + alice_wallet.coldkey.ss58_address: get_dynamic_balance( + balances[alice_wallet.coldkey.ss58_address].rao + ), + bob_wallet.coldkey.ss58_address: get_dynamic_balance( + balances[bob_wallet.coldkey.ss58_address].rao + ), + } + + assert balances == expected_balances + + alice_balance = balances[alice_wallet.coldkey.ss58_address] + + success = await async_subtensor.staking.add_stake_multiple( + alice_wallet, + hotkey_ss58s=[bob_wallet.hotkey.ss58_address for _ in netuids], + netuids=netuids, + amounts=[Balance.from_tao(10_000) for _ in netuids], + ) + + assert success is True + + stakes = [ + await async_subtensor.staking.get_stake( + alice_wallet.coldkey.ss58_address, + bob_wallet.hotkey.ss58_address, + netuid=netuid, + ) + for netuid in netuids + ] + + for netuid, stake in zip(netuids, stakes): + assert stake > Balance(0).set_unit(netuid), f"netuid={netuid} stake={stake}" + + alice_balance -= len(netuids) * Balance.from_tao(10_000) + + balances = await async_subtensor.wallets.get_balances( + alice_wallet.coldkey.ss58_address, + bob_wallet.coldkey.ss58_address, + ) + + expected_balances = { + alice_wallet.coldkey.ss58_address: get_dynamic_balance( + balances[alice_wallet.coldkey.ss58_address].rao + ), + bob_wallet.coldkey.ss58_address: get_dynamic_balance( + balances[bob_wallet.coldkey.ss58_address].rao + ), + } + + assert balances == expected_balances + + expected_fee_paid = Balance(0) + for netuid in netuids: + call = await async_subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="remove_stake", + call_params={ + "hotkey": bob_wallet.hotkey.ss58_address, + "amount_unstaked": Balance.from_tao(100).rao, + "netuid": netuid, + }, + ) + payment_info = await async_subtensor.substrate.get_payment_info( + call, alice_wallet.coldkeypub + ) + fee_alpha = Balance.from_rao(payment_info["partial_fee"]).set_unit(netuid) + dynamic_info = await async_subtensor.subnets.subnet(netuid) + fee_tao = dynamic_info.alpha_to_tao(fee_alpha) + expected_fee_paid += fee_tao + + success = await async_subtensor.staking.unstake_multiple( + wallet=alice_wallet, + netuids=netuids, + hotkey_ss58s=[bob_wallet.hotkey.ss58_address for _ in netuids], + amounts=[Balance.from_tao(100) for _ in netuids], + ) + + assert success is True + + for netuid, old_stake in zip(netuids, stakes): + stake = await async_subtensor.staking.get_stake( + coldkey_ss58=alice_wallet.coldkey.ss58_address, + hotkey_ss58=bob_wallet.hotkey.ss58_address, + netuid=netuid, + ) + + assert stake < old_stake, f"netuid={netuid} stake={stake}" + + balances = await async_subtensor.wallets.get_balances( + alice_wallet.coldkey.ss58_address, + bob_wallet.coldkey.ss58_address, + ) + + assert CloseInValue( # Make sure we are within 0.0001 TAO due to tx fees + balances[bob_wallet.coldkey.ss58_address], Balance.from_rao(100_000) + ) == Balance.from_tao(999_999.7994) + + assert balances[alice_wallet.coldkey.ss58_address] > alice_balance + logging.console.success("✅ Test [green]test_batch_operations_async[/green] passed") + + +def test_safe_staking_scenarios(subtensor, alice_wallet, bob_wallet, eve_wallet): + """ + Tests safe staking scenarios with different parameters. + + For both staking and unstaking: + 1. Fails with strict threshold (0.5%) and no partial staking + 2. Succeeds with strict threshold (0.5%) and partial staking allowed + 3. Succeeds with lenient threshold (10% and 30%) and no partial staking + """ + logging.console.info("Testing [blue]test_safe_staking_scenarios[/blue]") + + alice_subnet_netuid = subtensor.subnets.get_total_subnets() # 2 + # Register root as Alice - the subnet owner and validator + assert subtensor.extrinsics.register_subnet(alice_wallet) + + # Verify subnet created successfully + assert subtensor.subnets.subnet_exists(alice_subnet_netuid), ( + "Subnet wasn't created successfully" + ) + + # Change the tempo of the subnet + TEMPO_TO_SET = 100 if subtensor.chain.is_fast_blocks() else 20 + assert ( + sudo_set_admin_utils( + substrate=subtensor.substrate, + wallet=alice_wallet, + call_function="sudo_set_tempo", + call_params={"netuid": alice_subnet_netuid, "tempo": TEMPO_TO_SET}, + )[0] + is True + ) + tempo = subtensor.subnets.get_subnet_hyperparameters( + netuid=alice_subnet_netuid + ).tempo + assert tempo == TEMPO_TO_SET, "SN tempos has not been changed." + logging.console.success(f"SN #{alice_subnet_netuid} tempo set to {TEMPO_TO_SET}") + + assert wait_to_start_call(subtensor, alice_wallet, alice_subnet_netuid) + + subtensor.extrinsics.burned_register( + wallet=bob_wallet, + netuid=alice_subnet_netuid, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + initial_stake = subtensor.staking.get_stake( + coldkey_ss58=alice_wallet.coldkey.ss58_address, + hotkey_ss58=bob_wallet.hotkey.ss58_address, + netuid=alice_subnet_netuid, + ) + assert initial_stake == Balance(0).set_unit(alice_subnet_netuid) + logging.console.info(f"[orange]Initial stake: {initial_stake}[orange]") + + # Test Staking Scenarios + stake_amount = Balance.from_tao(100) + + # 1. Strict params - should fail + success = subtensor.staking.add_stake( + wallet=alice_wallet, + netuid=alice_subnet_netuid, + hotkey_ss58=bob_wallet.hotkey.ss58_address, + amount=stake_amount, + safe_staking=True, + rate_tolerance=0.005, # 0.5% + allow_partial_stake=False, + ) + assert success is False + + current_stake = subtensor.staking.get_stake( + coldkey_ss58=alice_wallet.coldkey.ss58_address, + hotkey_ss58=bob_wallet.hotkey.ss58_address, + netuid=alice_subnet_netuid, + ) + assert current_stake == Balance(0).set_unit(alice_subnet_netuid), ( + "Stake should not change after failed attempt" + ) + logging.console.info(f"[orange]Current stake: {current_stake}[orange]") + + # 2. Partial allowed - should succeed partially + success = subtensor.staking.add_stake( + wallet=alice_wallet, + netuid=alice_subnet_netuid, + hotkey_ss58=bob_wallet.hotkey.ss58_address, + amount=stake_amount, + safe_staking=True, + rate_tolerance=0.005, # 0.5% + allow_partial_stake=True, + ) + assert success is True + + partial_stake = subtensor.staking.get_stake( + coldkey_ss58=alice_wallet.coldkey.ss58_address, + hotkey_ss58=bob_wallet.hotkey.ss58_address, + netuid=alice_subnet_netuid, + ) + assert partial_stake > Balance(0).set_unit(alice_subnet_netuid), ( + "Partial stake should be added" + ) + assert partial_stake < stake_amount, ( + "Partial stake should be less than requested amount" + ) + + # 3. Higher threshold - should succeed fully + amount = Balance.from_tao(100) + success = subtensor.staking.add_stake( + wallet=alice_wallet, + netuid=alice_subnet_netuid, + hotkey_ss58=bob_wallet.hotkey.ss58_address, + amount=amount, + safe_staking=True, + rate_tolerance=0.22, # 22% + allow_partial_stake=False, + ) + assert success is True + + full_stake = subtensor.staking.get_stake( + coldkey_ss58=alice_wallet.coldkey.ss58_address, + hotkey_ss58=bob_wallet.hotkey.ss58_address, + netuid=alice_subnet_netuid, + ) + + # Test Unstaking Scenarios + # 1. Strict params - should fail + success = subtensor.staking.unstake( + wallet=alice_wallet, + netuid=alice_subnet_netuid, + hotkey_ss58=bob_wallet.hotkey.ss58_address, + amount=full_stake, + safe_unstaking=True, + rate_tolerance=0.005, # 0.5% + allow_partial_stake=False, + ) + assert success is False, "Unstake should fail." + + current_stake = subtensor.staking.get_stake( + coldkey_ss58=alice_wallet.coldkey.ss58_address, + hotkey_ss58=bob_wallet.hotkey.ss58_address, + netuid=alice_subnet_netuid, + ) + + logging.console.info(f"[orange]Current stake: {current_stake}[orange]") + logging.console.info(f"[orange]Full stake: {full_stake}[orange]") + + assert current_stake == full_stake, ( + "Stake should not change after failed unstake attempt" + ) + + # 2. Partial allowed - should succeed partially + success = subtensor.staking.unstake( + wallet=alice_wallet, + netuid=alice_subnet_netuid, + hotkey_ss58=bob_wallet.hotkey.ss58_address, + amount=current_stake, + safe_unstaking=True, + rate_tolerance=0.005, # 0.5% + allow_partial_stake=True, + ) + assert success is True + + partial_unstake = subtensor.staking.get_stake( + coldkey_ss58=alice_wallet.coldkey.ss58_address, + hotkey_ss58=bob_wallet.hotkey.ss58_address, + netuid=alice_subnet_netuid, + ) + logging.console.info(f"[orange]Partial unstake: {partial_unstake}[orange]") + assert partial_unstake > Balance(0).set_unit(alice_subnet_netuid), ( + "Some stake should remain" + ) + + # 3. Higher threshold - should succeed fully + success = subtensor.staking.unstake( + wallet=alice_wallet, + netuid=alice_subnet_netuid, + hotkey_ss58=bob_wallet.hotkey.ss58_address, + amount=partial_unstake, + safe_unstaking=True, + rate_tolerance=0.3, # 30% + allow_partial_stake=False, + ) + assert success is True, "Unstake should succeed" + logging.console.success("✅ Test [green]test_safe_staking_scenarios[/green] passed") + + +@pytest.mark.asyncio +async def test_safe_staking_scenarios_async( + async_subtensor, alice_wallet, bob_wallet, eve_wallet +): + """ + Tests safe staking scenarios with different parameters. + + For both staking and unstaking: + 1. Fails with strict threshold (0.5%) and no partial staking + 2. Succeeds with strict threshold (0.5%) and partial staking allowed + 3. Succeeds with lenient threshold (10% and 30%) and no partial staking + """ + logging.console.info("Testing [blue]test_safe_staking_scenarios_async[/blue]") + + alice_subnet_netuid = await async_subtensor.subnets.get_total_subnets() # 2 + # Register root as Alice - the subnet owner and validator + assert await async_subtensor.extrinsics.register_subnet(alice_wallet) + + # Verify subnet created successfully + assert await async_subtensor.subnets.subnet_exists(alice_subnet_netuid), ( + "Subnet wasn't created successfully" + ) + + # Change the tempo of the subnet + TEMPO_TO_SET = 100 if await async_subtensor.chain.is_fast_blocks() else 20 + assert ( + await async_sudo_set_admin_utils( + substrate=async_subtensor.substrate, + wallet=alice_wallet, + call_function="sudo_set_tempo", + call_params={"netuid": alice_subnet_netuid, "tempo": TEMPO_TO_SET}, + ) + )[0] is True + tempo = ( + await async_subtensor.subnets.get_subnet_hyperparameters( + netuid=alice_subnet_netuid + ) + ).tempo + assert tempo == TEMPO_TO_SET, "SN tempos has not been changed." + logging.console.success(f"SN #{alice_subnet_netuid} tempo set to {TEMPO_TO_SET}") + + assert await async_wait_to_start_call( + async_subtensor, alice_wallet, alice_subnet_netuid + ) + + await async_subtensor.extrinsics.burned_register( + wallet=bob_wallet, + netuid=alice_subnet_netuid, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + initial_stake = await async_subtensor.staking.get_stake( + coldkey_ss58=alice_wallet.coldkey.ss58_address, + hotkey_ss58=bob_wallet.hotkey.ss58_address, + netuid=alice_subnet_netuid, + ) + assert initial_stake == Balance(0).set_unit(alice_subnet_netuid) + logging.console.info(f"[orange]Initial stake: {initial_stake}[orange]") + + # Test Staking Scenarios + stake_amount = Balance.from_tao(100) + + # 1. Strict params - should fail + success = await async_subtensor.staking.add_stake( + wallet=alice_wallet, + netuid=alice_subnet_netuid, + hotkey_ss58=bob_wallet.hotkey.ss58_address, + amount=stake_amount, + safe_staking=True, + rate_tolerance=0.005, # 0.5% + allow_partial_stake=False, + ) + assert success is False + + current_stake = await async_subtensor.staking.get_stake( + coldkey_ss58=alice_wallet.coldkey.ss58_address, + hotkey_ss58=bob_wallet.hotkey.ss58_address, + netuid=alice_subnet_netuid, + ) + assert current_stake == Balance(0).set_unit(alice_subnet_netuid), ( + "Stake should not change after failed attempt" + ) + logging.console.info(f"[orange]Current stake: {current_stake}[orange]") + + # 2. Partial allowed - should succeed partially + success = await async_subtensor.staking.add_stake( + wallet=alice_wallet, + netuid=alice_subnet_netuid, + hotkey_ss58=bob_wallet.hotkey.ss58_address, + amount=stake_amount, + safe_staking=True, + rate_tolerance=0.005, # 0.5% + allow_partial_stake=True, + ) + assert success is True + + partial_stake = await async_subtensor.staking.get_stake( + coldkey_ss58=alice_wallet.coldkey.ss58_address, + hotkey_ss58=bob_wallet.hotkey.ss58_address, + netuid=alice_subnet_netuid, + ) + assert partial_stake > Balance(0).set_unit(alice_subnet_netuid), ( + "Partial stake should be added" + ) + assert partial_stake < stake_amount, ( + "Partial stake should be less than requested amount" + ) + + # 3. Higher threshold - should succeed fully + amount = Balance.from_tao(100) + success = await async_subtensor.staking.add_stake( + wallet=alice_wallet, + netuid=alice_subnet_netuid, + hotkey_ss58=bob_wallet.hotkey.ss58_address, + amount=amount, + safe_staking=True, + rate_tolerance=0.22, # 22% + allow_partial_stake=False, + ) + assert success is True + + full_stake = await async_subtensor.staking.get_stake( + coldkey_ss58=alice_wallet.coldkey.ss58_address, + hotkey_ss58=bob_wallet.hotkey.ss58_address, + netuid=alice_subnet_netuid, + ) + + # Test Unstaking Scenarios + # 1. Strict params - should fail + success = await async_subtensor.staking.unstake( + wallet=alice_wallet, + netuid=alice_subnet_netuid, + hotkey_ss58=bob_wallet.hotkey.ss58_address, + amount=full_stake, + safe_unstaking=True, + rate_tolerance=0.005, # 0.5% + allow_partial_stake=False, + ) + assert success is False, "Unstake should fail." + + current_stake = await async_subtensor.staking.get_stake( + coldkey_ss58=alice_wallet.coldkey.ss58_address, + hotkey_ss58=bob_wallet.hotkey.ss58_address, + netuid=alice_subnet_netuid, + ) + + logging.console.info(f"[orange]Current stake: {current_stake}[orange]") + logging.console.info(f"[orange]Full stake: {full_stake}[orange]") + + assert current_stake == full_stake, ( + "Stake should not change after failed unstake attempt" + ) + + # 2. Partial allowed - should succeed partially + success = await async_subtensor.staking.unstake( + wallet=alice_wallet, + netuid=alice_subnet_netuid, + hotkey_ss58=bob_wallet.hotkey.ss58_address, + amount=current_stake, + safe_unstaking=True, + rate_tolerance=0.005, # 0.5% + allow_partial_stake=True, + ) + assert success is True + + partial_unstake = await async_subtensor.staking.get_stake( + coldkey_ss58=alice_wallet.coldkey.ss58_address, + hotkey_ss58=bob_wallet.hotkey.ss58_address, + netuid=alice_subnet_netuid, + ) + logging.console.info(f"[orange]Partial unstake: {partial_unstake}[orange]") + assert partial_unstake > Balance(0).set_unit(alice_subnet_netuid), ( + "Some stake should remain" + ) + + # 3. Higher threshold - should succeed fully + success = await async_subtensor.staking.unstake( + wallet=alice_wallet, + netuid=alice_subnet_netuid, + hotkey_ss58=bob_wallet.hotkey.ss58_address, + amount=partial_unstake, + safe_unstaking=True, + rate_tolerance=0.3, # 30% + allow_partial_stake=False, + ) + assert success is True, "Unstake should succeed" + logging.console.success( + "✅ Test [green]test_safe_staking_scenarios_async[/green] passed" + ) + + +def test_safe_swap_stake_scenarios(subtensor, alice_wallet, bob_wallet): + """ + Tests safe swap stake scenarios with different parameters. + + Tests: + 1. Fails with strict threshold (0.5%) + 2. Succeeds with lenient threshold (10%) + """ + logging.console.info("Testing [blue]test_safe_swap_stake_scenarios[/blue]") + + # Create new subnet (netuid 2) and register Alice + origin_netuid = 2 + assert subtensor.subnets.register_subnet(bob_wallet) + assert subtensor.subnets.subnet_exists(origin_netuid), ( + "Subnet wasn't created successfully" + ) + dest_netuid = 3 + assert subtensor.subnets.register_subnet(bob_wallet) + assert subtensor.subnets.subnet_exists(dest_netuid), ( + "Subnet wasn't created successfully" + ) + + # make sure we passed start_call limit for both subnets + assert wait_to_start_call(subtensor, bob_wallet, origin_netuid) + assert wait_to_start_call(subtensor, bob_wallet, dest_netuid) + + # Register Alice on both subnets + subtensor.subnets.burned_register( + wallet=alice_wallet, + netuid=origin_netuid, + ) + subtensor.subnets.burned_register( + wallet=alice_wallet, + netuid=dest_netuid, + ) + + # Add initial stake to swap from + initial_stake_amount = Balance.from_tao(10_000) + success = subtensor.staking.add_stake( + wallet=alice_wallet, + netuid=origin_netuid, + hotkey_ss58=alice_wallet.hotkey.ss58_address, + amount=initial_stake_amount, + ) + assert success is True + + origin_stake = subtensor.staking.get_stake( + alice_wallet.coldkey.ss58_address, + alice_wallet.hotkey.ss58_address, + netuid=origin_netuid, + ) + assert origin_stake > Balance(0).set_unit(origin_netuid), ( + "Origin stake should be non-zero" + ) + + stake_swap_amount = Balance.from_tao(10_000) + # 1. Try swap with strict threshold and big amount- should fail + success = subtensor.staking.swap_stake( + wallet=alice_wallet, + hotkey_ss58=alice_wallet.hotkey.ss58_address, + origin_netuid=origin_netuid, + destination_netuid=dest_netuid, + amount=stake_swap_amount, + wait_for_inclusion=True, + wait_for_finalization=True, + safe_swapping=True, + rate_tolerance=0.005, # 0.5% + allow_partial_stake=False, + ) + assert success is False + + # Verify no stake was moved + dest_stake = subtensor.staking.get_stake( + alice_wallet.coldkey.ss58_address, + alice_wallet.hotkey.ss58_address, + netuid=dest_netuid, + ) + assert dest_stake == Balance(0).set_unit(dest_netuid), ( + "Destination stake should remain 0 after failed swap" + ) + + # 2. Try swap with higher threshold and less amount - should succeed + stake_swap_amount = Balance.from_tao(100) + success = subtensor.staking.swap_stake( + wallet=alice_wallet, + hotkey_ss58=alice_wallet.hotkey.ss58_address, + origin_netuid=origin_netuid, + destination_netuid=dest_netuid, + amount=stake_swap_amount, + wait_for_inclusion=True, + wait_for_finalization=True, + safe_swapping=True, + rate_tolerance=0.3, # 30% + allow_partial_stake=True, + ) + assert success is True + + # Verify stake was moved + dest_stake = subtensor.staking.get_stake( + alice_wallet.coldkey.ss58_address, + alice_wallet.hotkey.ss58_address, + netuid=dest_netuid, + ) + assert dest_stake > Balance(0).set_unit(dest_netuid), ( + "Destination stake should be non-zero after successful swap" + ) + logging.console.success( + "✅ Test [green]test_safe_swap_stake_scenarios[/green] passed" + ) + + +@pytest.mark.asyncio +async def test_safe_swap_stake_scenarios_async( + async_subtensor, alice_wallet, bob_wallet +): + """ + Tests safe swap stake scenarios with different parameters. + + Tests: + 1. Fails with strict threshold (0.5%) + 2. Succeeds with lenient threshold (10%) + """ + logging.console.info("Testing [blue]test_safe_swap_stake_scenarios_async[/blue]") + + # Create new subnet (netuid 2) and register Alice + origin_netuid = 2 + assert await async_subtensor.subnets.register_subnet(bob_wallet) + assert await async_subtensor.subnets.subnet_exists(origin_netuid), ( + "Subnet wasn't created successfully" + ) + dest_netuid = 3 + assert await async_subtensor.subnets.register_subnet(bob_wallet) + assert await async_subtensor.subnets.subnet_exists(dest_netuid), ( + "Subnet wasn't created successfully" + ) + + # make sure we passed start_call limit for both subnets + assert await async_wait_to_start_call(async_subtensor, bob_wallet, origin_netuid) + assert await async_wait_to_start_call(async_subtensor, bob_wallet, dest_netuid) + + # Register Alice on both subnets + await async_subtensor.subnets.burned_register( + wallet=alice_wallet, + netuid=origin_netuid, + ) + await async_subtensor.subnets.burned_register( + wallet=alice_wallet, + netuid=dest_netuid, + ) + + # Add initial stake to swap from + initial_stake_amount = Balance.from_tao(10_000) + success = await async_subtensor.staking.add_stake( + wallet=alice_wallet, + netuid=origin_netuid, + hotkey_ss58=alice_wallet.hotkey.ss58_address, + amount=initial_stake_amount, + ) + assert success is True + + origin_stake = await async_subtensor.staking.get_stake( + alice_wallet.coldkey.ss58_address, + alice_wallet.hotkey.ss58_address, + netuid=origin_netuid, + ) + assert origin_stake > Balance(0).set_unit(origin_netuid), ( + "Origin stake should be non-zero" + ) + + stake_swap_amount = Balance.from_tao(10_000) + # 1. Try swap with strict threshold and big amount- should fail + success = await async_subtensor.staking.swap_stake( + wallet=alice_wallet, + hotkey_ss58=alice_wallet.hotkey.ss58_address, + origin_netuid=origin_netuid, + destination_netuid=dest_netuid, + amount=stake_swap_amount, + wait_for_inclusion=True, + wait_for_finalization=True, + safe_swapping=True, + rate_tolerance=0.005, # 0.5% + allow_partial_stake=False, + ) + assert success is False + + # Verify no stake was moved + dest_stake = await async_subtensor.staking.get_stake( + alice_wallet.coldkey.ss58_address, + alice_wallet.hotkey.ss58_address, + netuid=dest_netuid, + ) + assert dest_stake == Balance(0).set_unit(dest_netuid), ( + "Destination stake should remain 0 after failed swap" + ) + + # 2. Try swap with higher threshold and less amount - should succeed + stake_swap_amount = Balance.from_tao(100) + success = await async_subtensor.staking.swap_stake( + wallet=alice_wallet, + hotkey_ss58=alice_wallet.hotkey.ss58_address, + origin_netuid=origin_netuid, + destination_netuid=dest_netuid, + amount=stake_swap_amount, + wait_for_inclusion=True, + wait_for_finalization=True, + safe_swapping=True, + rate_tolerance=0.3, # 30% + allow_partial_stake=True, + ) + assert success is True + + # Verify stake was moved + dest_stake = await async_subtensor.staking.get_stake( + alice_wallet.coldkey.ss58_address, + alice_wallet.hotkey.ss58_address, + netuid=dest_netuid, + ) + assert dest_stake > Balance(0).set_unit(dest_netuid), ( + "Destination stake should be non-zero after successful swap" + ) + logging.console.success( + "✅ Test [green]test_safe_swap_stake_scenarios_async[/green] passed" + ) + + +def test_move_stake(subtensor, alice_wallet, bob_wallet, dave_wallet): + """ + Tests: + - Adding stake + - Moving stake from one hotkey-subnet pair to another + - Testing `move_stake` method with `move_all_stake=True` flag. + """ + logging.console.info("Testing [blue]test_move_stake[/blue]") + + alice_subnet_netuid = subtensor.subnets.get_total_subnets() # 2 + assert subtensor.subnets.register_subnet(alice_wallet) + assert subtensor.subnets.subnet_exists(alice_subnet_netuid), ( + "Subnet wasn't created successfully" + ) + + assert wait_to_start_call(subtensor, alice_wallet, alice_subnet_netuid) + + assert subtensor.staking.add_stake( + wallet=alice_wallet, + netuid=alice_subnet_netuid, + hotkey_ss58=alice_wallet.hotkey.ss58_address, + amount=Balance.from_tao(1_000), + ) + + stakes = subtensor.staking.get_stake_for_coldkey(alice_wallet.coldkey.ss58_address) + + assert stakes == [ + StakeInfo( + hotkey_ss58=alice_wallet.hotkey.ss58_address, + coldkey_ss58=alice_wallet.coldkey.ss58_address, + netuid=alice_subnet_netuid, + stake=get_dynamic_balance(stakes[0].stake.rao, alice_subnet_netuid), + locked=Balance(0).set_unit(alice_subnet_netuid), + emission=get_dynamic_balance(stakes[0].emission.rao, alice_subnet_netuid), + drain=0, + is_registered=True, + ), + ] + + bob_subnet_netuid = subtensor.subnets.get_total_subnets() # 3 + subtensor.subnets.register_subnet(bob_wallet) + assert subtensor.subnets.subnet_exists(bob_subnet_netuid), ( + "Subnet wasn't created successfully" + ) + + assert wait_to_start_call(subtensor, bob_wallet, bob_subnet_netuid) + + subtensor.subnets.burned_register( + wallet=bob_wallet, + netuid=alice_subnet_netuid, + ) + + subtensor.subnets.burned_register( + wallet=dave_wallet, + netuid=alice_subnet_netuid, + ) + + assert subtensor.staking.move_stake( + wallet=alice_wallet, + origin_hotkey_ss58=alice_wallet.hotkey.ss58_address, + origin_netuid=alice_subnet_netuid, + destination_hotkey_ss58=bob_wallet.hotkey.ss58_address, + destination_netuid=bob_subnet_netuid, + amount=stakes[0].stake, + wait_for_finalization=True, + wait_for_inclusion=True, + ) + + stakes = subtensor.staking.get_stake_for_coldkey(alice_wallet.coldkey.ss58_address) + + expected_stakes = [ + StakeInfo( + hotkey_ss58=stakes[0].hotkey_ss58, + coldkey_ss58=alice_wallet.coldkey.ss58_address, + netuid=alice_subnet_netuid + if subtensor.chain.is_fast_blocks() + else bob_subnet_netuid, + stake=get_dynamic_balance(stakes[0].stake.rao, bob_subnet_netuid), + locked=Balance(0).set_unit(bob_subnet_netuid), + emission=get_dynamic_balance(stakes[0].emission.rao, bob_subnet_netuid), + drain=0, + is_registered=True, + ) + ] + + fast_block_stake = ( + [ + StakeInfo( + hotkey_ss58=stakes[1].hotkey_ss58, + coldkey_ss58=alice_wallet.coldkey.ss58_address, + netuid=bob_subnet_netuid, + stake=get_dynamic_balance(stakes[1].stake.rao, bob_subnet_netuid), + locked=Balance(0).set_unit(bob_subnet_netuid), + emission=get_dynamic_balance(stakes[1].emission.rao, bob_subnet_netuid), + drain=0, + is_registered=True, + ), + ] + if subtensor.chain.is_fast_blocks() + else [] + ) + + expected_stakes += fast_block_stake + assert stakes == expected_stakes + + # test move_stake with move_all_stake=True + dave_stake = subtensor.staking.get_stake( + coldkey_ss58=dave_wallet.coldkey.ss58_address, + hotkey_ss58=bob_wallet.hotkey.ss58_address, + netuid=bob_subnet_netuid, + ) + logging.console.info(f"[orange]Dave stake before adding: {dave_stake}[orange]") + + assert subtensor.staking.add_stake( + wallet=dave_wallet, + netuid=bob_subnet_netuid, + hotkey_ss58=dave_wallet.hotkey.ss58_address, + amount=Balance.from_tao(1000), + allow_partial_stake=True, + ) + + dave_stake = subtensor.staking.get_stake( + coldkey_ss58=dave_wallet.coldkey.ss58_address, + hotkey_ss58=dave_wallet.hotkey.ss58_address, + netuid=bob_subnet_netuid, + ) + logging.console.info(f"[orange]Dave stake after adding: {dave_stake}[orange]") + + # let chain to process the transaction + subtensor.wait_for_block( + subtensor.block + subtensor.subnets.tempo(netuid=bob_subnet_netuid) + ) + + assert subtensor.staking.move_stake( + wallet=dave_wallet, + origin_hotkey_ss58=dave_wallet.hotkey.ss58_address, + origin_netuid=bob_subnet_netuid, + destination_hotkey_ss58=bob_wallet.hotkey.ss58_address, + destination_netuid=bob_subnet_netuid, + wait_for_inclusion=True, + wait_for_finalization=True, + move_all_stake=True, + ) + + dave_stake = subtensor.staking.get_stake( + coldkey_ss58=dave_wallet.coldkey.ss58_address, + hotkey_ss58=dave_wallet.hotkey.ss58_address, + netuid=bob_subnet_netuid, + ) + logging.console.info(f"[orange]Dave stake after moving all: {dave_stake}[orange]") + + assert dave_stake.rao == CloseInValue(0, 0.00001) + + logging.console.success("✅ Test [green]test_move_stake[/green] passed.") + + +@pytest.mark.asyncio +async def test_move_stake_async(async_subtensor, alice_wallet, bob_wallet, dave_wallet): + """ + Tests: + - Adding stake + - Moving stake from one hotkey-subnet pair to another + - Testing `move_stake` method with `move_all_stake=True` flag. + """ + logging.console.info("Testing [blue]test_move_stake_async[/blue]") + + alice_subnet_netuid = await async_subtensor.subnets.get_total_subnets() # 2 + assert await async_subtensor.subnets.register_subnet(alice_wallet) + assert await async_subtensor.subnets.subnet_exists(alice_subnet_netuid), ( + "Subnet wasn't created successfully" + ) + + assert await async_wait_to_start_call( + async_subtensor, alice_wallet, alice_subnet_netuid + ) + + assert await async_subtensor.staking.add_stake( + wallet=alice_wallet, + netuid=alice_subnet_netuid, + hotkey_ss58=alice_wallet.hotkey.ss58_address, + amount=Balance.from_tao(1_000), + ) + + stakes = await async_subtensor.staking.get_stake_for_coldkey( + alice_wallet.coldkey.ss58_address + ) + + assert stakes == [ + StakeInfo( + hotkey_ss58=alice_wallet.hotkey.ss58_address, + coldkey_ss58=alice_wallet.coldkey.ss58_address, + netuid=alice_subnet_netuid, + stake=get_dynamic_balance(stakes[0].stake.rao, alice_subnet_netuid), + locked=Balance(0).set_unit(alice_subnet_netuid), + emission=get_dynamic_balance(stakes[0].emission.rao, alice_subnet_netuid), + drain=0, + is_registered=True, + ), + ] + + bob_subnet_netuid = await async_subtensor.subnets.get_total_subnets() # 3 + await async_subtensor.subnets.register_subnet(bob_wallet) + assert await async_subtensor.subnets.subnet_exists(bob_subnet_netuid), ( + "Subnet wasn't created successfully" + ) + + assert await async_wait_to_start_call( + async_subtensor, bob_wallet, bob_subnet_netuid + ) + + await async_subtensor.subnets.burned_register( + wallet=bob_wallet, + netuid=alice_subnet_netuid, + ) + + await async_subtensor.subnets.burned_register( + wallet=dave_wallet, + netuid=alice_subnet_netuid, + ) + + assert await async_subtensor.staking.move_stake( + wallet=alice_wallet, + origin_hotkey_ss58=alice_wallet.hotkey.ss58_address, + origin_netuid=alice_subnet_netuid, + destination_hotkey_ss58=bob_wallet.hotkey.ss58_address, + destination_netuid=bob_subnet_netuid, + amount=stakes[0].stake, + wait_for_finalization=True, + wait_for_inclusion=True, + ) + + stakes = await async_subtensor.staking.get_stake_for_coldkey( + alice_wallet.coldkey.ss58_address + ) + + expected_stakes = [ + StakeInfo( + hotkey_ss58=stakes[0].hotkey_ss58, + coldkey_ss58=alice_wallet.coldkey.ss58_address, + netuid=alice_subnet_netuid + if await async_subtensor.chain.is_fast_blocks() + else bob_subnet_netuid, + stake=get_dynamic_balance(stakes[0].stake.rao, bob_subnet_netuid), + locked=Balance(0).set_unit(bob_subnet_netuid), + emission=get_dynamic_balance(stakes[0].emission.rao, bob_subnet_netuid), + drain=0, + is_registered=True, + ) + ] + + fast_block_stake = ( + [ + StakeInfo( + hotkey_ss58=stakes[1].hotkey_ss58, + coldkey_ss58=alice_wallet.coldkey.ss58_address, + netuid=bob_subnet_netuid, + stake=get_dynamic_balance(stakes[1].stake.rao, bob_subnet_netuid), + locked=Balance(0).set_unit(bob_subnet_netuid), + emission=get_dynamic_balance(stakes[1].emission.rao, bob_subnet_netuid), + drain=0, + is_registered=True, + ), + ] + if await async_subtensor.chain.is_fast_blocks() + else [] + ) + + expected_stakes += fast_block_stake + assert stakes == expected_stakes + + # test move_stake with move_all_stake=True + dave_stake = await async_subtensor.staking.get_stake( + coldkey_ss58=dave_wallet.coldkey.ss58_address, + hotkey_ss58=bob_wallet.hotkey.ss58_address, + netuid=bob_subnet_netuid, + ) + logging.console.info(f"[orange]Dave stake before adding: {dave_stake}[orange]") + + assert await async_subtensor.staking.add_stake( + wallet=dave_wallet, + hotkey_ss58=dave_wallet.hotkey.ss58_address, + netuid=bob_subnet_netuid, + amount=Balance.from_tao(1000), + allow_partial_stake=True, + ) + + dave_stake = await async_subtensor.staking.get_stake( + coldkey_ss58=dave_wallet.coldkey.ss58_address, + hotkey_ss58=dave_wallet.hotkey.ss58_address, + netuid=bob_subnet_netuid, + ) + logging.console.info(f"[orange]Dave stake after adding: {dave_stake}[orange]") + + block_, tampo_ = await asyncio.gather( + async_subtensor.block, async_subtensor.subnets.tempo(netuid=bob_subnet_netuid) + ) + # let chain to process the transaction + await async_subtensor.wait_for_block(block_ + tampo_) + + assert await async_subtensor.staking.move_stake( + wallet=dave_wallet, + origin_hotkey_ss58=dave_wallet.hotkey.ss58_address, + origin_netuid=bob_subnet_netuid, + destination_hotkey_ss58=bob_wallet.hotkey.ss58_address, + destination_netuid=bob_subnet_netuid, + wait_for_inclusion=True, + wait_for_finalization=True, + move_all_stake=True, + ) + + dave_stake = await async_subtensor.staking.get_stake( + coldkey_ss58=dave_wallet.coldkey.ss58_address, + hotkey_ss58=dave_wallet.hotkey.ss58_address, + netuid=bob_subnet_netuid, + ) + logging.console.info(f"[orange]Dave stake after moving all: {dave_stake}[orange]") + + assert dave_stake.rao == CloseInValue(0, 0.00001) + + logging.console.success("✅ Test [green]test_move_stake_async[/green] passed.") + + +def test_transfer_stake(subtensor, alice_wallet, bob_wallet, dave_wallet): + """ + Tests: + - Adding stake + - Transferring stake from one coldkey-subnet pair to another + """ + logging.console.info("Testing [blue]test_transfer_stake[/blue]") + + alice_subnet_netuid = subtensor.subnets.get_total_subnets() # 2 + + assert subtensor.subnets.register_subnet(alice_wallet) + assert subtensor.subnets.subnet_exists(alice_subnet_netuid), ( + "Subnet wasn't created successfully" + ) + + assert wait_to_start_call(subtensor, alice_wallet, alice_subnet_netuid) + + subtensor.subnets.burned_register( + wallet=alice_wallet, + netuid=alice_subnet_netuid, + ) + + assert subtensor.staking.add_stake( + wallet=alice_wallet, + netuid=alice_subnet_netuid, + hotkey_ss58=alice_wallet.hotkey.ss58_address, + amount=Balance.from_tao(1_000), + ) + + alice_stakes = subtensor.staking.get_stake_for_coldkey( + alice_wallet.coldkey.ss58_address + ) + + assert alice_stakes == [ + StakeInfo( + hotkey_ss58=alice_wallet.hotkey.ss58_address, + coldkey_ss58=alice_wallet.coldkey.ss58_address, + netuid=alice_subnet_netuid, + stake=get_dynamic_balance(alice_stakes[0].stake.rao, alice_subnet_netuid), + locked=Balance(0).set_unit(alice_subnet_netuid), + emission=get_dynamic_balance( + alice_stakes[0].emission.rao, alice_subnet_netuid + ), + drain=0, + is_registered=True, + ), + ] + + bob_stakes = subtensor.staking.get_stake_for_coldkey( + bob_wallet.coldkey.ss58_address + ) + + assert bob_stakes == [] + + dave_subnet_netuid = subtensor.subnets.get_total_subnets() # 3 + subtensor.subnets.register_subnet(dave_wallet) + + assert wait_to_start_call(subtensor, dave_wallet, dave_subnet_netuid) + + subtensor.subnets.burned_register( + wallet=bob_wallet, + netuid=dave_subnet_netuid, + ) + + assert subtensor.staking.transfer_stake( + alice_wallet, + destination_coldkey_ss58=bob_wallet.coldkey.ss58_address, + hotkey_ss58=alice_wallet.hotkey.ss58_address, + origin_netuid=alice_subnet_netuid, + destination_netuid=dave_subnet_netuid, + amount=alice_stakes[0].stake, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + alice_stakes = subtensor.staking.get_stake_for_coldkey( + alice_wallet.coldkey.ss58_address + ) + + expected_alice_stake = ( + [ + StakeInfo( + hotkey_ss58=alice_wallet.hotkey.ss58_address, + coldkey_ss58=alice_wallet.coldkey.ss58_address, + netuid=alice_subnet_netuid, + stake=get_dynamic_balance( + alice_stakes[0].stake.rao, alice_subnet_netuid + ), + locked=Balance(0).set_unit(alice_subnet_netuid), + emission=get_dynamic_balance( + alice_stakes[0].emission.rao, alice_subnet_netuid + ), + drain=0, + is_registered=True, + ), + ] + if subtensor.chain.is_fast_blocks() + else [] + ) + + assert alice_stakes == expected_alice_stake + + bob_stakes = subtensor.staking.get_stake_for_coldkey( + bob_wallet.coldkey.ss58_address + ) + + expected_bob_stake = [ + StakeInfo( + hotkey_ss58=alice_wallet.hotkey.ss58_address, + coldkey_ss58=bob_wallet.coldkey.ss58_address, + netuid=dave_subnet_netuid, + stake=get_dynamic_balance(bob_stakes[0].stake.rao, dave_subnet_netuid), + locked=Balance(0).set_unit(dave_subnet_netuid), + emission=get_dynamic_balance( + bob_stakes[0].emission.rao, dave_subnet_netuid + ), + drain=0, + is_registered=False, + ), + ] + assert bob_stakes == expected_bob_stake + logging.console.success("✅ Test [green]test_transfer_stake[/green] passed") + + +@pytest.mark.asyncio +async def test_transfer_stake_async( + async_subtensor, alice_wallet, bob_wallet, dave_wallet +): + """ + Tests: + - Adding stake + - Transferring stake from one coldkey-subnet pair to another + """ + logging.console.info("Testing [blue]test_transfer_stake_async[/blue]") + + alice_subnet_netuid = await async_subtensor.subnets.get_total_subnets() # 2 + + assert await async_subtensor.subnets.register_subnet(alice_wallet) + assert await async_subtensor.subnets.subnet_exists(alice_subnet_netuid), ( + "Subnet wasn't created successfully" + ) + + assert await async_wait_to_start_call( + async_subtensor, alice_wallet, alice_subnet_netuid + ) + + await async_subtensor.subnets.burned_register( + wallet=alice_wallet, + netuid=alice_subnet_netuid, + ) + + assert await async_subtensor.staking.add_stake( + wallet=alice_wallet, + netuid=alice_subnet_netuid, + hotkey_ss58=alice_wallet.hotkey.ss58_address, + amount=Balance.from_tao(1_000), + ) + + alice_stakes = await async_subtensor.staking.get_stake_for_coldkey( + alice_wallet.coldkey.ss58_address + ) + + assert alice_stakes == [ + StakeInfo( + hotkey_ss58=alice_wallet.hotkey.ss58_address, + coldkey_ss58=alice_wallet.coldkey.ss58_address, + netuid=alice_subnet_netuid, + stake=get_dynamic_balance(alice_stakes[0].stake.rao, alice_subnet_netuid), + locked=Balance(0).set_unit(alice_subnet_netuid), + emission=get_dynamic_balance( + alice_stakes[0].emission.rao, alice_subnet_netuid + ), + drain=0, + is_registered=True, + ), + ] + + bob_stakes = await async_subtensor.staking.get_stake_for_coldkey( + bob_wallet.coldkey.ss58_address + ) + + assert bob_stakes == [] + + dave_subnet_netuid = await async_subtensor.subnets.get_total_subnets() # 3 + await async_subtensor.subnets.register_subnet(dave_wallet) + + assert await async_wait_to_start_call( + async_subtensor, dave_wallet, dave_subnet_netuid + ) + + await async_subtensor.subnets.burned_register( + wallet=bob_wallet, + netuid=dave_subnet_netuid, + ) + + assert await async_subtensor.staking.transfer_stake( + alice_wallet, + destination_coldkey_ss58=bob_wallet.coldkey.ss58_address, + hotkey_ss58=alice_wallet.hotkey.ss58_address, + origin_netuid=alice_subnet_netuid, + destination_netuid=dave_subnet_netuid, + amount=alice_stakes[0].stake, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + alice_stakes = await async_subtensor.staking.get_stake_for_coldkey( + alice_wallet.coldkey.ss58_address + ) + + expected_alice_stake = ( + [ + StakeInfo( + hotkey_ss58=alice_wallet.hotkey.ss58_address, + coldkey_ss58=alice_wallet.coldkey.ss58_address, + netuid=alice_subnet_netuid, + stake=get_dynamic_balance( + alice_stakes[0].stake.rao, alice_subnet_netuid + ), + locked=Balance(0).set_unit(alice_subnet_netuid), + emission=get_dynamic_balance( + alice_stakes[0].emission.rao, alice_subnet_netuid + ), + drain=0, + is_registered=True, + ), + ] + if await async_subtensor.chain.is_fast_blocks() + else [] + ) + + assert alice_stakes == expected_alice_stake + + bob_stakes = await async_subtensor.staking.get_stake_for_coldkey( + bob_wallet.coldkey.ss58_address + ) + + expected_bob_stake = [ + StakeInfo( + hotkey_ss58=alice_wallet.hotkey.ss58_address, + coldkey_ss58=bob_wallet.coldkey.ss58_address, + netuid=dave_subnet_netuid, + stake=get_dynamic_balance(bob_stakes[0].stake.rao, dave_subnet_netuid), + locked=Balance(0).set_unit(dave_subnet_netuid), + emission=get_dynamic_balance( + bob_stakes[0].emission.rao, dave_subnet_netuid + ), + drain=0, + is_registered=False, + ), + ] + assert bob_stakes == expected_bob_stake + logging.console.success("✅ Test [green]test_transfer_stake_async[/green] passed") + + +# For test we set rate_tolerance=0.7 (70%) because of price is highly dynamic for fast-blocks and 2 SN to avoid ` +# Slippage is too high for the transaction`. This logic controls by the chain. +# Also this test implementation works with non-fast-blocks run. +@pytest.mark.parametrize( + "rate_tolerance", + [None, 1.0], + ids=[ + "Without price limit", + "With price limit", + ], +) +def test_unstaking_with_limit( + subtensor, alice_wallet, bob_wallet, dave_wallet, rate_tolerance +): + """Test unstaking with limits goes well for all subnets with and without price limit.""" + logging.console.info("Testing [blue]test_unstaking_with_limit[/blue]") + + # Register first SN + alice_subnet_netuid_2 = subtensor.subnets.get_total_subnets() # 2 + assert subtensor.subnets.register_subnet(alice_wallet) + assert subtensor.subnets.subnet_exists(alice_subnet_netuid_2), ( + "Subnet wasn't created successfully" + ) + + wait_to_start_call(subtensor, alice_wallet, alice_subnet_netuid_2) + + # Register Bob and Dave in SN2 + assert subtensor.subnets.burned_register( + wallet=bob_wallet, + netuid=alice_subnet_netuid_2, + ) + + assert subtensor.subnets.burned_register( + wallet=dave_wallet, + netuid=alice_subnet_netuid_2, + ) + + # Register second SN + alice_subnet_netuid_3 = subtensor.subnets.get_total_subnets() # 3 + assert subtensor.subnets.register_subnet(alice_wallet) + assert subtensor.subnets.subnet_exists(alice_subnet_netuid_3), ( + "Subnet wasn't created successfully" + ) + + wait_to_start_call(subtensor, alice_wallet, alice_subnet_netuid_3) + + # Register Bob and Dave in SN3 + assert subtensor.subnets.burned_register( + wallet=bob_wallet, + netuid=alice_subnet_netuid_3, + ) + + assert subtensor.subnets.burned_register( + wallet=dave_wallet, + netuid=alice_subnet_netuid_3, + ) + + # Check Bob's stakes are empty. + assert ( + subtensor.staking.get_stake_info_for_coldkey(bob_wallet.coldkey.ss58_address) + == [] + ) + + # Bob stakes to Dave in both SNs + + assert subtensor.staking.add_stake( + wallet=bob_wallet, + hotkey_ss58=dave_wallet.hotkey.ss58_address, + netuid=alice_subnet_netuid_2, + amount=Balance.from_tao(10000), + period=16, + ), f"Cant add stake to dave in SN {alice_subnet_netuid_2}" + assert subtensor.staking.add_stake( + wallet=bob_wallet, + hotkey_ss58=alice_wallet.hotkey.ss58_address, + netuid=alice_subnet_netuid_3, + amount=Balance.from_tao(15000), + period=16, + ), f"Cant add stake to dave in SN {alice_subnet_netuid_3}" + + # Check that both stakes are presented in result + bob_stakes = subtensor.staking.get_stake_info_for_coldkey( + bob_wallet.coldkey.ss58_address + ) + assert len(bob_stakes) == 2 + + if rate_tolerance == 0.0001: + # Raise the error + with pytest.raises( + ChainError, match="Slippage is too high for the transaction" + ): + subtensor.staking.unstake_all( + wallet=bob_wallet, + hotkey=bob_stakes[0].hotkey_ss58, + netuid=bob_stakes[0].netuid, + rate_tolerance=rate_tolerance, + ) + else: + # Successful cases + for si in bob_stakes: + assert subtensor.staking.unstake_all( + wallet=bob_wallet, + hotkey=si.hotkey_ss58, + netuid=si.netuid, + rate_tolerance=rate_tolerance, + )[0] + + # Make sure both unstake were successful. + bob_stakes = subtensor.staking.get_stake_info_for_coldkey( + bob_wallet.coldkey.ss58_address + ) + assert len(bob_stakes) == 0 + + +@pytest.mark.parametrize( + "rate_tolerance", + [None, 1.0], + ids=[ + "Without price limit", + "With price limit", + ], +) +@pytest.mark.asyncio +async def test_unstaking_with_limit_async( + async_subtensor, alice_wallet, bob_wallet, dave_wallet, rate_tolerance +): + """Test unstaking with limits goes well for all subnets with and without price limit.""" + logging.console.info("Testing [blue]test_unstaking_with_limit_async[/blue]") + + # Register first SN + alice_subnet_netuid_2 = await async_subtensor.subnets.get_total_subnets() # 2 + assert await async_subtensor.subnets.register_subnet(alice_wallet) + assert await async_subtensor.subnets.subnet_exists(alice_subnet_netuid_2), ( + "Subnet wasn't created successfully" + ) + + assert await async_wait_to_start_call( + async_subtensor, alice_wallet, alice_subnet_netuid_2 + ) + + # Register Bob and Dave in SN2 + assert await async_subtensor.subnets.burned_register( + wallet=bob_wallet, + netuid=alice_subnet_netuid_2, + ) + + assert await async_subtensor.subnets.burned_register( + wallet=dave_wallet, + netuid=alice_subnet_netuid_2, + ) + + # Register second SN + alice_subnet_netuid_3 = await async_subtensor.subnets.get_total_subnets() # 3 + assert await async_subtensor.subnets.register_subnet(alice_wallet) + assert await async_subtensor.subnets.subnet_exists(alice_subnet_netuid_3), ( + "Subnet wasn't created successfully" + ) + + await async_wait_to_start_call(async_subtensor, alice_wallet, alice_subnet_netuid_3) + + # Register Bob and Dave in SN3 + assert await async_subtensor.subnets.burned_register( + wallet=bob_wallet, + netuid=alice_subnet_netuid_3, + ) + + assert await async_subtensor.subnets.burned_register( + wallet=dave_wallet, + netuid=alice_subnet_netuid_3, + ) + + # Check Bob's stakes are empty. + assert ( + await async_subtensor.staking.get_stake_info_for_coldkey( + bob_wallet.coldkey.ss58_address + ) + == [] + ) + + # Bob stakes to Dave in both SNs + + assert await async_subtensor.staking.add_stake( + wallet=bob_wallet, + netuid=alice_subnet_netuid_2, + hotkey_ss58=dave_wallet.hotkey.ss58_address, + amount=Balance.from_tao(10000), + period=16, + ), f"Cant add stake to dave in SN {alice_subnet_netuid_2}" + assert await async_subtensor.staking.add_stake( + wallet=bob_wallet, + netuid=alice_subnet_netuid_3, + hotkey_ss58=alice_wallet.hotkey.ss58_address, + amount=Balance.from_tao(15000), + period=16, + ), f"Cant add stake to dave in SN {alice_subnet_netuid_3}" + + # Check that both stakes are presented in result + bob_stakes = await async_subtensor.staking.get_stake_info_for_coldkey( + bob_wallet.coldkey.ss58_address + ) + assert len(bob_stakes) == 2 + + if rate_tolerance == 0.0001: + # Raise the error + with pytest.raises( + ChainError, match="Slippage is too high for the transaction" + ): + await async_subtensor.staking.unstake_all( + wallet=bob_wallet, + netuid=bob_stakes[0].netuid, + hotkey=bob_stakes[0].hotkey_ss58, + rate_tolerance=rate_tolerance, + ) + else: + # Successful cases + for si in bob_stakes: + assert ( + await async_subtensor.staking.unstake_all( + wallet=bob_wallet, + netuid=si.netuid, + hotkey=si.hotkey_ss58, + rate_tolerance=rate_tolerance, + ) + )[0] + + # Make sure both unstake were successful. + bob_stakes = await async_subtensor.staking.get_stake_info_for_coldkey( + bob_wallet.coldkey.ss58_address + ) + assert len(bob_stakes) == 0 + + logging.console.success( + "✅ Test [green]test_unstaking_with_limit_async[/green] passed" + ) diff --git a/tests/e2e_tests/test_subnets.py b/tests/e2e_tests/test_subnets.py new file mode 100644 index 0000000000..89028d2d06 --- /dev/null +++ b/tests/e2e_tests/test_subnets.py @@ -0,0 +1,65 @@ +import pytest +from bittensor.utils.btlogging import logging + + +def test_subnets(subtensor, alice_wallet): + """ + Tests: + - Querying subnets + - Filtering subnets + - Checks default TxRateLimit + """ + logging.console.info("Testing [blue]test_subnets[/blue]") + + subnets = subtensor.subnets.all_subnets() + assert len(subnets) == 2 + + subtensor.subnets.register_subnet(alice_wallet) + + subnets = subtensor.subnets.all_subnets() + assert len(subnets) == 3 + + netuids = subtensor.wallets.filter_netuids_by_registered_hotkeys( + all_netuids=[0, 1, 2], + filter_for_netuids=[2], + all_hotkeys=[alice_wallet], + block=subtensor.block, + ) + assert netuids == [2] + + tx_rate_limit = subtensor.chain.tx_rate_limit() + assert tx_rate_limit == 1000 + + logging.console.success("✅ Test [green]test_subnets[/green] passed") + + +@pytest.mark.asyncio +async def test_subnets_async(async_subtensor, alice_wallet): + """ + Async tests: + - Querying subnets + - Filtering subnets + - Checks default TxRateLimit + """ + logging.console.info("Testing [blue]test_subnets_async[/blue]") + + subnets = await async_subtensor.subnets.all_subnets() + assert len(subnets) == 2 + + assert await async_subtensor.subnets.register_subnet(alice_wallet) + + subnets = await async_subtensor.subnets.all_subnets() + assert len(subnets) == 3 + + netuids = await async_subtensor.wallets.filter_netuids_by_registered_hotkeys( + all_netuids=[0, 1, 2], + filter_for_netuids=[2], + all_hotkeys=[alice_wallet], + block=await async_subtensor.block, + ) + assert netuids == [2] + + tx_rate_limit = await async_subtensor.chain.tx_rate_limit() + assert tx_rate_limit == 1000 + + logging.console.success("✅ Test [green]test_subnets_async[/green] passed") diff --git a/tests/e2e_tests/test_subtensor_functions.py b/tests/e2e_tests/test_subtensor_functions.py new file mode 100644 index 0000000000..669b929d3f --- /dev/null +++ b/tests/e2e_tests/test_subtensor_functions.py @@ -0,0 +1,415 @@ +import asyncio + +import pytest +from bittensor.utils.btlogging import logging +from bittensor.utils.balance import Balance +from tests.e2e_tests.utils.chain_interactions import ( + async_wait_epoch, + wait_epoch, +) +from tests.e2e_tests.utils.e2e_test_utils import ( + async_wait_to_start_call, + wait_to_start_call, +) + +""" +Verifies: + +* get_subnets() +* get_total_subnets() +* subnet_exists() +* get_netuids_for_hotkey() +* is_hotkey_registered_any() +* is_hotkey_registered_on_subnet() +* get_uid_for_hotkey_on_subnet() +* get_neuron_for_pubkey_and_subnet() +* get_balance() +* get_subnet_burn_cost() +* difficulty() +* burned_register() +* recycle() +* get_existential_deposit() +* get_all_subnets_info() +""" + + +@pytest.mark.asyncio +async def test_subtensor_extrinsics(subtensor, templates, alice_wallet, bob_wallet): + """ + Tests subtensor extrinsics + + Steps: + 1. Validate subnets in the chain before/after registering netuid = 1 + 2. Register Alice's neuron + 3. Verify Alice and Bob's participation in subnets (individually and global) + 4. Verify uids of Alice and Bob gets populated correctly + 5. Start Alice as a validator and verify NeuronInfo before/after is different + Raises: + AssertionError: If any of the checks or verifications fail + """ + logging.console.info("Testing [blue]test_subtensor_extrinsics[/blue]") + netuid = subtensor.subnets.get_total_subnets() # 22 + # Initial balance for Alice, defined in the genesis file of localnet + initial_alice_balance = Balance.from_tao(1_000_000) + # Current Existential deposit for all accounts in bittensor + existential_deposit = Balance.from_tao(0.000_000_500) + + # Subnets 0 and 1 are bootstrapped from the start + assert subtensor.subnets.get_subnets() == [0, 1] + assert subtensor.subnets.get_total_subnets() == 2 + + # Assert correct balance is fetched for Alice + alice_balance = subtensor.wallets.get_balance(alice_wallet.coldkeypub.ss58_address) + assert alice_balance == initial_alice_balance, ( + "Balance for Alice wallet doesn't match with pre-def value" + ) + + # Subnet burn cost is initially lower before we register a subnet + pre_subnet_creation_cost = subtensor.subnets.get_subnet_burn_cost() + + # Register subnet + assert subtensor.subnets.register_subnet(alice_wallet), ( + "Unable to register the subnet" + ) + + # Subnet burn cost is increased immediately after a subnet is registered + post_subnet_creation_cost = subtensor.subnets.get_subnet_burn_cost() + + # Assert that the burn cost changed after registering a subnet + assert Balance.from_tao(pre_subnet_creation_cost) < Balance.from_tao( + post_subnet_creation_cost + ), "Burn cost did not change after subnet creation" + + # Assert amount is deducted once a subnetwork is registered by Alice + alice_balance_post_sn = subtensor.wallets.get_balance( + alice_wallet.coldkeypub.ss58_address + ) + assert alice_balance_post_sn + pre_subnet_creation_cost == initial_alice_balance, ( + "Balance is the same even after registering a subnet" + ) + + # Subnet 2 is added after registration + assert subtensor.subnets.get_subnets() == [0, 1, 2] + assert subtensor.subnets.get_total_subnets() == 3 + + # Verify subnet 2 created successfully + assert subtensor.subnets.subnet_exists(netuid) + + # Default subnetwork difficulty + assert subtensor.subnets.difficulty(netuid) == 10_000_000, ( + "Couldn't fetch correct subnet difficulty" + ) + + # Verify Alice is registered to netuid 2 and Bob isn't registered to any + assert subtensor.wallets.get_netuids_for_hotkey( + hotkey_ss58=alice_wallet.hotkey.ss58_address + ) == [ + netuid, + ], "Alice is not registered to netuid 2 as expected" + assert ( + subtensor.wallets.get_netuids_for_hotkey( + hotkey_ss58=bob_wallet.hotkey.ss58_address + ) + == [] + ), "Bob is unexpectedly registered to some netuid" + + # Verify Alice's hotkey is registered to any subnet (currently netuid = 2) + assert subtensor.wallets.is_hotkey_registered_any( + hotkey_ss58=alice_wallet.hotkey.ss58_address + ), "Alice's hotkey is not registered to any subnet" + assert not subtensor.wallets.is_hotkey_registered_any( + hotkey_ss58=bob_wallet.hotkey.ss58_address + ), "Bob's hotkey is unexpectedly registered to a subnet" + + # Verify netuid = 2 only has Alice registered and not Bob + assert subtensor.wallets.is_hotkey_registered_on_subnet( + netuid=netuid, hotkey_ss58=alice_wallet.hotkey.ss58_address + ), "Alice's hotkey is not registered on netuid 1" + assert not subtensor.wallets.is_hotkey_registered_on_subnet( + netuid=netuid, hotkey_ss58=bob_wallet.hotkey.ss58_address + ), "Bob's hotkey is unexpectedly registered on netuid 1" + + # Verify Alice's UID on netuid 2 is 0 + assert ( + subtensor.subnets.get_uid_for_hotkey_on_subnet( + hotkey_ss58=alice_wallet.hotkey.ss58_address, netuid=netuid + ) + == 0 + ), "UID for Alice's hotkey on netuid 2 is not 0 as expected" + + bob_balance = subtensor.wallets.get_balance(bob_wallet.coldkeypub.ss58_address) + + assert wait_to_start_call(subtensor, alice_wallet, netuid) + + # Register Bob to the subnet + assert subtensor.subnets.burned_register(bob_wallet, netuid), ( + "Unable to register Bob as a neuron" + ) + + # Verify Bob's UID on netuid 2 is 1 + assert ( + subtensor.subnets.get_uid_for_hotkey_on_subnet( + hotkey_ss58=bob_wallet.hotkey.ss58_address, netuid=netuid + ) + == 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) + call = subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="burned_register", + call_params={ + "netuid": netuid, + "hotkey": bob_wallet.hotkey.ss58_address, + }, + ) + payment_info = subtensor.substrate.get_payment_info(call, bob_wallet.coldkeypub) + fee = Balance.from_rao(payment_info["partial_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" + ) + + # neuron_info_old = subtensor.get_neuron_for_pubkey_and_subnet( + # alice_wallet.hotkey.ss58_address, netuid=netuid + # ) + + async with templates.validator(alice_wallet, netuid): + await asyncio.sleep( + 5 + ) # wait for 5 seconds for the metagraph and subtensor to refresh with latest data + + await wait_epoch(subtensor, netuid) + + # Verify neuron info is updated after running as a validator + # neuron_info = subtensor.get_neuron_for_pubkey_and_subnet( + # alice_wallet.hotkey.ss58_address, netuid=netuid + # ) + # assert ( + # neuron_info_old.dividends != neuron_info.dividends + # ), "Neuron info not updated after running validator" + + # Fetch and assert existential deposit for an account in the network + assert subtensor.chain.get_existential_deposit() == existential_deposit, ( + "Existential deposit value doesn't match with pre-defined value" + ) + + # Fetching all subnets in the network + all_subnets = subtensor.subnets.get_all_subnets_info() + + # Assert all netuids are present in all_subnets + expected_netuids = [0, 1, 2] + actual_netuids = [subnet.netuid for subnet in all_subnets] + assert actual_netuids == expected_netuids, ( + f"Expected netuids {expected_netuids}, but found {actual_netuids}" + ) + + # Assert that the owner_ss58 of subnet 2 matches Alice's coldkey address + expected_owner = alice_wallet.coldkeypub.ss58_address + subnet_2 = next((subnet for subnet in all_subnets if subnet.netuid == netuid), None) + actual_owner = subnet_2.owner_ss58 + assert actual_owner == expected_owner, ( + f"Expected owner {expected_owner}, but found {actual_owner}" + ) + + logging.console.success("✅ Passed [blue]test_subtensor_extrinsics[/blue]") + + +@pytest.mark.asyncio +async def test_subtensor_extrinsics_async( + async_subtensor, templates, alice_wallet, bob_wallet +): + """ + Tests subtensor extrinsics + + Steps: + 1. Validate subnets in the chain before/after registering netuid = 1 + 2. Register Alice's neuron + 3. Verify Alice and Bob's participation in subnets (individually and global) + 4. Verify uids of Alice and Bob gets populated correctly + 5. Start Alice as a validator and verify NeuronInfo before/after is different + Raises: + AssertionError: If any of the checks or verifications fail + """ + logging.console.info("Testing [blue]test_subtensor_extrinsics[/blue]") + netuid = await async_subtensor.subnets.get_total_subnets() # 22 + # Initial balance for Alice, defined in the genesis file of localnet + initial_alice_balance = Balance.from_tao(1_000_000) + # Current Existential deposit for all accounts in bittensor + existential_deposit = Balance.from_tao(0.000_000_500) + + # Subnets 0 and 1 are bootstrapped from the start + assert await async_subtensor.subnets.get_subnets() == [0, 1] + assert await async_subtensor.subnets.get_total_subnets() == 2 + + # Assert correct balance is fetched for Alice + alice_balance = await async_subtensor.wallets.get_balance( + alice_wallet.coldkeypub.ss58_address + ) + assert alice_balance == initial_alice_balance, ( + "Balance for Alice wallet doesn't match with pre-def value" + ) + + # Subnet burn cost is initially lower before we register a subnet + pre_subnet_creation_cost = await async_subtensor.subnets.get_subnet_burn_cost() + + # Register subnet + assert await async_subtensor.subnets.register_subnet(alice_wallet), ( + "Unable to register the subnet" + ) + + # Subnet burn cost is increased immediately after a subnet is registered + post_subnet_creation_cost = await async_subtensor.subnets.get_subnet_burn_cost() + + # Assert that the burn cost changed after registering a subnet + assert Balance.from_tao(pre_subnet_creation_cost) < Balance.from_tao( + post_subnet_creation_cost + ), "Burn cost did not change after subnet creation" + + # Assert amount is deducted once a subnetwork is registered by Alice + alice_balance_post_sn = await async_subtensor.wallets.get_balance( + alice_wallet.coldkeypub.ss58_address + ) + assert alice_balance_post_sn + pre_subnet_creation_cost == initial_alice_balance, ( + "Balance is the same even after registering a subnet" + ) + + # Subnet 2 is added after registration + assert await async_subtensor.subnets.get_subnets() == [0, 1, 2] + assert await async_subtensor.subnets.get_total_subnets() == 3 + + # Verify subnet 2 created successfully + assert await async_subtensor.subnets.subnet_exists(netuid) + + # Default subnetwork difficulty + assert await async_subtensor.subnets.difficulty(netuid) == 10_000_000, ( + "Couldn't fetch correct subnet difficulty" + ) + + # Verify Alice is registered to netuid 2 and Bob isn't registered to any + assert await async_subtensor.wallets.get_netuids_for_hotkey( + hotkey_ss58=alice_wallet.hotkey.ss58_address + ) == [ + netuid, + ], "Alice is not registered to netuid 2 as expected" + assert ( + await async_subtensor.wallets.get_netuids_for_hotkey( + hotkey_ss58=bob_wallet.hotkey.ss58_address + ) + == [] + ), "Bob is unexpectedly registered to some netuid" + + # Verify Alice's hotkey is registered to any subnet (currently netuid = 2) + assert await async_subtensor.wallets.is_hotkey_registered_any( + hotkey_ss58=alice_wallet.hotkey.ss58_address + ), "Alice's hotkey is not registered to any subnet" + assert not await async_subtensor.wallets.is_hotkey_registered_any( + hotkey_ss58=bob_wallet.hotkey.ss58_address + ), "Bob's hotkey is unexpectedly registered to a subnet" + + # Verify netuid = 2 only has Alice registered and not Bob + assert await async_subtensor.wallets.is_hotkey_registered_on_subnet( + netuid=netuid, hotkey_ss58=alice_wallet.hotkey.ss58_address + ), "Alice's hotkey is not registered on netuid 1" + assert not await async_subtensor.wallets.is_hotkey_registered_on_subnet( + netuid=netuid, hotkey_ss58=bob_wallet.hotkey.ss58_address + ), "Bob's hotkey is unexpectedly registered on netuid 1" + + # Verify Alice's UID on netuid 2 is 0 + assert ( + await async_subtensor.subnets.get_uid_for_hotkey_on_subnet( + hotkey_ss58=alice_wallet.hotkey.ss58_address, netuid=netuid + ) + == 0 + ), "UID for Alice's hotkey on netuid 2 is not 0 as expected" + + bob_balance = await async_subtensor.wallets.get_balance( + bob_wallet.coldkeypub.ss58_address + ) + + assert await async_wait_to_start_call(async_subtensor, alice_wallet, netuid) + + # Register Bob to the subnet + assert await async_subtensor.subnets.burned_register(bob_wallet, netuid), ( + "Unable to register Bob as a neuron" + ) + + # Verify Bob's UID on netuid 2 is 1 + assert ( + await async_subtensor.subnets.get_uid_for_hotkey_on_subnet( + hotkey_ss58=bob_wallet.hotkey.ss58_address, netuid=netuid + ) + == 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) + call = await async_subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="burned_register", + call_params={ + "netuid": netuid, + "hotkey": bob_wallet.hotkey.ss58_address, + }, + ) + payment_info = await async_subtensor.substrate.get_payment_info( + call, bob_wallet.coldkeypub + ) + fee = Balance.from_rao(payment_info["partial_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" + ) + + # neuron_info_old = subtensor.get_neuron_for_pubkey_and_subnet( + # alice_wallet.hotkey.ss58_address, netuid=netuid + # ) + + async with templates.validator(alice_wallet, netuid): + await asyncio.sleep( + 5 + ) # wait for 5 seconds for the metagraph and subtensor to refresh with latest data + + await async_wait_epoch(async_subtensor, netuid) + + # Verify neuron info is updated after running as a validator + # neuron_info = subtensor.get_neuron_for_pubkey_and_subnet( + # alice_wallet.hotkey.ss58_address, netuid=netuid + # ) + # assert ( + # neuron_info_old.dividends != neuron_info.dividends + # ), "Neuron info not updated after running validator" + + # Fetch and assert existential deposit for an account in the network + assert ( + await async_subtensor.chain.get_existential_deposit() == existential_deposit + ), "Existential deposit value doesn't match with pre-defined value" + + # Fetching all subnets in the network + all_subnets = await async_subtensor.subnets.get_all_subnets_info() + + # Assert all netuids are present in all_subnets + expected_netuids = [0, 1, 2] + actual_netuids = [subnet.netuid for subnet in all_subnets] + assert actual_netuids == expected_netuids, ( + f"Expected netuids {expected_netuids}, but found {actual_netuids}" + ) + + # Assert that the owner_ss58 of subnet 2 matches Alice's coldkey address + expected_owner = alice_wallet.coldkeypub.ss58_address + subnet_2 = next((subnet for subnet in all_subnets if subnet.netuid == netuid), None) + actual_owner = subnet_2.owner_ss58 + assert actual_owner == expected_owner, ( + f"Expected owner {expected_owner}, but found {actual_owner}" + ) + + logging.console.success("✅ Passed [blue]test_subtensor_extrinsics[/blue]") diff --git a/tests/e2e_tests/test_transfer.py b/tests/e2e_tests/test_transfer.py new file mode 100644 index 0000000000..89613f6553 --- /dev/null +++ b/tests/e2e_tests/test_transfer.py @@ -0,0 +1,204 @@ +import typing + +from bittensor_wallet import Wallet +import pytest + +from bittensor.utils.balance import Balance +from bittensor import logging + +if typing.TYPE_CHECKING: + from bittensor.core.subtensor_api import SubtensorApi + +logging.set_trace() + + +def test_transfer(subtensor, alice_wallet): + """ + Test the transfer mechanism on the chain + + Steps: + 1. Calculate existing balance and transfer 2 Tao + 2. Calculate balance after transfer call and verify calculations + Raises: + AssertionError: If any of the checks or verifications fail + """ + logging.console.info("Testing [blue]test_transfer[/blue]") + + transfer_value = Balance.from_tao(2) + dest_coldkey = "5GpzQgpiAKHMWNSH3RN4GLf96GVTDct9QxYEFAY7LWcVzTbx" + + # Fetch transfer fee + transfer_fee = subtensor.wallets.get_transfer_fee( + wallet=alice_wallet, + dest=dest_coldkey, + value=transfer_value, + ) + + # Account details before transfer + balance_before = subtensor.wallets.get_balance(alice_wallet.coldkeypub.ss58_address) + + # Transfer Tao + assert subtensor.extrinsics.transfer( + wallet=alice_wallet, + destination=dest_coldkey, + amount=transfer_value, + wait_for_finalization=True, + wait_for_inclusion=True, + ) + # Account details after transfer + balance_after = subtensor.wallets.get_balance(alice_wallet.coldkeypub.ss58_address) + + # Assert correct transfer calculations + assert balance_before - transfer_fee - transfer_value == balance_after, ( + f"Expected {balance_before - transfer_value - transfer_fee}, got {balance_after}" + ) + + logging.console.success("✅ Passed [blue]test_transfer[/blue]") + + +@pytest.mark.asyncio +async def test_transfer_async(async_subtensor, alice_wallet): + """ + Test the transfer mechanism on the chain + + Steps: + 1. Calculate existing balance and transfer 2 Tao + 2. Calculate balance after transfer call and verify calculations + Raises: + AssertionError: If any of the checks or verifications fail + """ + logging.console.info("Testing [blue]test_transfer[/blue]") + + transfer_value = Balance.from_tao(2) + dest_coldkey = "5GpzQgpiAKHMWNSH3RN4GLf96GVTDct9QxYEFAY7LWcVzTbx" + + # Fetch transfer fee + transfer_fee = await async_subtensor.wallets.get_transfer_fee( + wallet=alice_wallet, + dest=dest_coldkey, + value=transfer_value, + ) + + # Account details before transfer + balance_before = await async_subtensor.wallets.get_balance( + alice_wallet.coldkeypub.ss58_address + ) + + # Transfer Tao + assert await async_subtensor.extrinsics.transfer( + wallet=alice_wallet, + destination=dest_coldkey, + amount=transfer_value, + wait_for_finalization=True, + wait_for_inclusion=True, + ) + # Account details after transfer + balance_after = await async_subtensor.wallets.get_balance( + alice_wallet.coldkeypub.ss58_address + ) + + # Assert correct transfer calculations + assert balance_before - transfer_fee - transfer_value == balance_after, ( + f"Expected {balance_before - transfer_value - transfer_fee}, got {balance_after}" + ) + + logging.console.success("✅ Passed [blue]test_transfer[/blue]") + + +def test_transfer_all(subtensor, alice_wallet): + logging.console.info("Testing [blue]test_transfer_all[/blue]") + + # create two dummy accounts we can drain + dummy_account_1 = Wallet(path="/tmp/bittensor-dummy-account-1") + dummy_account_2 = Wallet(path="/tmp/bittensor-dummy-account-2") + dummy_account_1.create_new_coldkey(use_password=False, overwrite=True) + dummy_account_2.create_new_coldkey(use_password=False, overwrite=True) + + # fund the first dummy account + assert subtensor.extrinsics.transfer( + wallet=alice_wallet, + destination=dummy_account_1.coldkeypub.ss58_address, + amount=Balance.from_tao(2.0), + wait_for_finalization=True, + wait_for_inclusion=True, + ) + # Account details before transfer + existential_deposit = subtensor.chain.get_existential_deposit() + assert subtensor.extrinsics.transfer( + wallet=dummy_account_1, + destination=dummy_account_2.coldkeypub.ss58_address, + amount=None, + transfer_all=True, + wait_for_finalization=True, + wait_for_inclusion=True, + keep_alive=True, + ) + balance_after = subtensor.wallets.get_balance( + dummy_account_1.coldkeypub.ss58_address + ) + assert balance_after == existential_deposit + assert subtensor.extrinsics.transfer( + wallet=dummy_account_2, + destination=alice_wallet.coldkeypub.ss58_address, + amount=None, + transfer_all=True, + wait_for_inclusion=True, + wait_for_finalization=True, + keep_alive=False, + ) + balance_after = subtensor.wallets.get_balance( + dummy_account_2.coldkeypub.ss58_address + ) + assert balance_after == Balance(0) + + logging.console.success("✅ Test [green]test_transfer_all[/green] passed.") + + +@pytest.mark.asyncio +async def test_transfer_all_async(async_subtensor, alice_wallet): + # create two dummy accounts we can drain + logging.console.info("Testing [blue]test_transfer_async[/blue]") + + dummy_account_1 = Wallet(path="/tmp/bittensor-dummy-account-3") + dummy_account_2 = Wallet(path="/tmp/bittensor-dummy-account-4") + dummy_account_1.create_new_coldkey(use_password=False, overwrite=True) + dummy_account_2.create_new_coldkey(use_password=False, overwrite=True) + + # fund the first dummy account + assert await async_subtensor.extrinsics.transfer( + wallet=alice_wallet, + destination=dummy_account_1.coldkeypub.ss58_address, + amount=Balance.from_tao(2.0), + wait_for_finalization=True, + wait_for_inclusion=True, + ) + # Account details before transfer + existential_deposit = await async_subtensor.chain.get_existential_deposit() + assert await async_subtensor.extrinsics.transfer( + wallet=dummy_account_1, + destination=dummy_account_2.coldkeypub.ss58_address, + amount=None, + transfer_all=True, + wait_for_finalization=True, + wait_for_inclusion=True, + keep_alive=True, + ) + balance_after = await async_subtensor.wallets.get_balance( + dummy_account_1.coldkeypub.ss58_address + ) + assert balance_after == existential_deposit + assert await async_subtensor.extrinsics.transfer( + wallet=dummy_account_2, + destination=alice_wallet.coldkeypub.ss58_address, + amount=None, + transfer_all=True, + wait_for_inclusion=True, + wait_for_finalization=True, + keep_alive=False, + ) + balance_after = await async_subtensor.wallets.get_balance( + dummy_account_2.coldkeypub.ss58_address + ) + assert balance_after == Balance(0) + + logging.console.success("✅ Test [green]test_transfer_async[/green] passed.") diff --git a/tests/e2e_tests/utils/chain_interactions.py b/tests/e2e_tests/utils/chain_interactions.py new file mode 100644 index 0000000000..edbb8305bc --- /dev/null +++ b/tests/e2e_tests/utils/chain_interactions.py @@ -0,0 +1,566 @@ +""" +This module provides functions interacting with the chain for end-to-end testing; +these are not present in btsdk but are required for e2e tests +""" + +import asyncio +import functools +import time +from typing import Union, Optional, TYPE_CHECKING + +from bittensor.utils.balance import Balance +from bittensor.utils.btlogging import logging + +# for typing purposes +if TYPE_CHECKING: + from bittensor import Wallet + from bittensor.core.subtensor_api import SubtensorApi + from async_substrate_interface import ( + AsyncSubstrateInterface, + AsyncExtrinsicReceipt, + SubstrateInterface, + ExtrinsicReceipt, + ) + + +def get_dynamic_balance(rao: int, netuid: int = 0): + """Returns a Balance object with the given rao and netuid for testing purposes with dynamic values.""" + return Balance.from_rao(rao).set_unit(netuid) + + +def sudo_set_hyperparameter_bool( + substrate: "SubstrateInterface", + wallet: "Wallet", + call_function: str, + value: bool, + netuid: int, +) -> bool: + """Sets boolean hyperparameter value through AdminUtils. Mimics setting hyperparams.""" + call = substrate.compose_call( + call_module="AdminUtils", + call_function=call_function, + call_params={"netuid": netuid, "enabled": value}, + ) + extrinsic = substrate.create_signed_extrinsic(call=call, keypair=wallet.coldkey) + response = substrate.submit_extrinsic( + extrinsic, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + return response.is_success + + +async def async_sudo_set_hyperparameter_bool( + substrate: "AsyncSubstrateInterface", + wallet: "Wallet", + call_function: str, + value: bool, + netuid: int, +) -> bool: + """Sets boolean hyperparameter value through AdminUtils. Mimics setting hyperparams.""" + call = await substrate.compose_call( + call_module="AdminUtils", + call_function=call_function, + call_params={"netuid": netuid, "enabled": value}, + ) + extrinsic = await substrate.create_signed_extrinsic( + call=call, keypair=wallet.coldkey + ) + response = await substrate.submit_extrinsic( + extrinsic, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + return await response.is_success + + +def sudo_set_hyperparameter_values( + substrate: "SubstrateInterface", + wallet: "Wallet", + call_function: str, + call_params: dict, + return_error_message: bool = False, +) -> Union[bool, tuple[bool, Optional[str]]]: + """Sets liquid alpha values using AdminUtils. Mimics setting hyperparams.""" + call = substrate.compose_call( + call_module="AdminUtils", + call_function=call_function, + call_params=call_params, + ) + extrinsic = substrate.create_signed_extrinsic(call=call, keypair=wallet.coldkey) + response = substrate.submit_extrinsic( + extrinsic, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + if return_error_message: + return response.is_success, response.error_message + + return response.is_success + + +async def async_sudo_set_hyperparameter_values( + substrate: "AsyncSubstrateInterface", + wallet: "Wallet", + call_function: str, + call_params: dict, + return_error_message: bool = False, +) -> Union[bool, tuple[bool, Optional[str]]]: + """Sets liquid alpha values using AdminUtils. Mimics setting hyperparams.""" + call = await substrate.compose_call( + call_module="AdminUtils", + call_function=call_function, + call_params=call_params, + ) + extrinsic = await substrate.create_signed_extrinsic( + call=call, keypair=wallet.coldkey + ) + response = await substrate.submit_extrinsic( + extrinsic=extrinsic, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + if return_error_message: + return await response.is_success, await response.error_message + + return await response.is_success + + +async def wait_epoch(subtensor: "SubtensorApi", netuid: int = 1, **kwargs): + """ + Waits for the next epoch to start on a specific subnet. + + Queries the tempo value from the Subtensor module and calculates the + interval based on the tempo. Then waits for the next epoch to start + by monitoring the current block number. + + Raises: + Exception: If the tempo cannot be determined from the chain. + """ + q_tempo = [ + v for (k, v) in subtensor.queries.query_map_subtensor("Tempo") if k == netuid + ] + if len(q_tempo) == 0: + raise Exception("could not determine tempo") + tempo = q_tempo[0].value + logging.info(f"tempo = {tempo}") + await wait_interval(tempo, subtensor, netuid, **kwargs) + + +async def async_wait_epoch(async_subtensor: "SubtensorApi", netuid: int = 1, **kwargs): + """ + Waits for the next epoch to start on a specific subnet. + + Queries the tempo value from the Subtensor module and calculates the + interval based on the tempo. Then waits for the next epoch to start + by monitoring the current block number. + + Raises: + Exception: If the tempo cannot be determined from the chain. + """ + q_tempo = [ + v + async for (k, v) in await async_subtensor.queries.query_map_subtensor("Tempo") + if k == netuid + ] + if len(q_tempo) == 0: + raise Exception("could not determine tempo") + tempo = q_tempo[0].value + logging.info(f"tempo = {tempo}") + await async_wait_interval(tempo, async_subtensor, netuid, **kwargs) + + +def next_tempo(current_block: int, tempo: int) -> int: + """ + Calculates the next tempo block for a specific subnet. + + Args: + current_block: The current block number. + tempo: The tempo value for the subnet. + + Returns: + int: The next tempo block number. + """ + return ((current_block // tempo) + 1) * tempo + 1 + + +async def wait_interval( + tempo: int, + subtensor: "SubtensorApi", + netuid: int = 1, + reporting_interval: int = 1, + sleep: float = 0.25, + times: int = 1, +): + """ + Waits until the next tempo interval starts for a specific subnet. + + Calculates the next tempo block start based on the current block number + and the provided tempo, then enters a loop where it periodically checks + the current block number until the next tempo interval starts. + """ + current_block = subtensor.chain.get_current_block() + next_tempo_block_start = current_block + + for _ in range(times): + next_tempo_block_start = next_tempo(next_tempo_block_start, tempo) + + last_reported = None + + while current_block < next_tempo_block_start: + await asyncio.sleep( + sleep, + ) # Wait before checking the block number again + current_block = subtensor.chain.get_current_block() + if last_reported is None or current_block - last_reported >= reporting_interval: + last_reported = current_block + print( + f"Current Block: {current_block} Next tempo for netuid {netuid} at: {next_tempo_block_start}" + ) + logging.info( + f"Current Block: {current_block} Next tempo for netuid {netuid} at: {next_tempo_block_start}" + ) + + +async def async_wait_interval( + tempo: int, + subtensor: "SubtensorApi", + netuid: int = 1, + reporting_interval: int = 1, + sleep: float = 0.25, + times: int = 1, +): + """ + Waits until the next tempo interval starts for a specific subnet. + + Calculates the next tempo block start based on the current block number + and the provided tempo, then enters a loop where it periodically checks + the current block number until the next tempo interval starts. + """ + current_block = await subtensor.chain.get_current_block() + next_tempo_block_start = current_block + + for _ in range(times): + next_tempo_block_start = next_tempo(next_tempo_block_start, tempo) + + last_reported = None + + while current_block < next_tempo_block_start: + await asyncio.sleep( + sleep, + ) # Wait before checking the block number again + current_block = await subtensor.chain.get_current_block() + if last_reported is None or current_block - last_reported >= reporting_interval: + last_reported = current_block + print( + f"Current Block: {current_block} Next tempo for netuid {netuid} at: {next_tempo_block_start}" + ) + logging.info( + f"Current Block: {current_block} Next tempo for netuid {netuid} at: {next_tempo_block_start}" + ) + + +def execute_and_wait_for_next_nonce( + subtensor: "SubtensorApi", wallet, sleep=0.25, timeout=60.0, max_retries=3 +): + """Decorator that ensures the nonce has been consumed after a blockchain extrinsic call.""" + + def decorator(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + for attempt in range(max_retries): + start_nonce = subtensor.substrate.get_account_next_index( + wallet.hotkey.ss58_address + ) + + result = func(*args, **kwargs) + + start_time = time.time() + + while time.time() - start_time < timeout: + current_nonce = subtensor.substrate.get_account_next_index( + wallet.hotkey.ss58_address + ) + + if current_nonce != start_nonce: + logging.console.info( + f"✅ Nonce changed from {start_nonce} to {current_nonce}" + ) + return result + + logging.console.info( + f"⏳ Waiting for nonce increment. Current: {current_nonce}" + ) + time.sleep(sleep) + + logging.warning( + f"⚠️ Attempt {attempt + 1}/{max_retries}: Nonce did not increment." + ) + raise TimeoutError(f"❌ Nonce did not change after {max_retries} attempts.") + + return wrapper + + return decorator + + +# Helper to execute sudo wrapped calls on the chain +def sudo_set_admin_utils( + substrate: "SubstrateInterface", + wallet: "Wallet", + call_function: str, + call_params: dict, + call_module: str = "AdminUtils", +) -> tuple[bool, Optional[dict]]: + """ + Wraps the call in sudo to set hyperparameter values using AdminUtils. + + Args: + substrate: Substrate connection. + wallet: Wallet object with the keypair for signing. + call_function: The AdminUtils function to call. + call_params: Parameters for the AdminUtils function. + call_module: The AdminUtils module to call. Defaults to "AdminUtils". + + Returns: + tuple: (success status, error details). + """ + inner_call = substrate.compose_call( + call_module=call_module, + call_function=call_function, + call_params=call_params, + ) + + sudo_call = substrate.compose_call( + call_module="Sudo", + call_function="sudo", + call_params={"call": inner_call}, + ) + extrinsic = substrate.create_signed_extrinsic( + call=sudo_call, keypair=wallet.coldkey + ) + response: "ExtrinsicReceipt" = substrate.submit_extrinsic( + extrinsic, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + return response.is_success, response.error_message + + +async def async_sudo_set_admin_utils( + substrate: "AsyncSubstrateInterface", + wallet: "Wallet", + call_function: str, + call_params: dict, + call_module: str = "AdminUtils", +) -> tuple[bool, Optional[dict]]: + """ + Wraps the call in sudo to set hyperparameter values using AdminUtils. + + Parameters: + substrate: Substrate connection. + wallet: Wallet object with the keypair for signing. + call_function: The AdminUtils function to call. + call_params: Parameters for the AdminUtils function. + call_module: The AdminUtils module to call. Defaults to "AdminUtils". + + Returns: + tuple: (success status, error details). + """ + inner_call = await substrate.compose_call( + call_module=call_module, + call_function=call_function, + call_params=call_params, + ) + + sudo_call = await substrate.compose_call( + call_module="Sudo", + call_function="sudo", + call_params={"call": inner_call}, + ) + extrinsic = await substrate.create_signed_extrinsic( + call=sudo_call, keypair=wallet.coldkey + ) + response: "AsyncExtrinsicReceipt" = await substrate.submit_extrinsic( + extrinsic, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + return await response.is_success, await response.error_message + + +def root_set_subtensor_hyperparameter_values( + substrate: "SubstrateInterface", + wallet: "Wallet", + call_function: str, + call_params: dict, +) -> tuple[bool, Optional[dict]]: + """Sets liquid alpha values using AdminUtils. Mimics setting hyperparams.""" + call = substrate.compose_call( + call_module="SubtensorModule", + call_function=call_function, + call_params=call_params, + ) + extrinsic = substrate.create_signed_extrinsic(call=call, keypair=wallet.coldkey) + + response: "ExtrinsicReceipt" = substrate.submit_extrinsic( + extrinsic, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + return response.is_success, response.error_message + + +def set_identity( + subtensor: "SubtensorApi", + wallet, + name="", + url="", + github_repo="", + image="", + discord="", + description="", + additional="", +): + return subtensor.sign_and_send_extrinsic( + subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="set_identity", + call_params={ + "name": name, + "url": url, + "github_repo": github_repo, + "image": image, + "discord": discord, + "description": description, + "additional": additional, + }, + ), + wallet, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + +async def async_set_identity( + subtensor: "SubtensorApi", + wallet: "Wallet", + name="", + url="", + github_repo="", + image="", + discord="", + description="", + additional="", +): + return await subtensor.sign_and_send_extrinsic( + await subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="set_identity", + call_params={ + "name": name, + "url": url, + "github_repo": github_repo, + "image": image, + "discord": discord, + "description": description, + "additional": additional, + }, + ), + wallet, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + +def propose(subtensor, wallet, proposal, duration): + return subtensor.sign_and_send_extrinsic( + subtensor.substrate.compose_call( + call_module="Triumvirate", + call_function="propose", + call_params={ + "proposal": proposal, + "length_bound": len(proposal.data), + "duration": duration, + }, + ), + wallet, + wait_for_finalization=True, + wait_for_inclusion=True, + ) + + +async def async_propose( + subtensor: "SubtensorApi", + wallet: "Wallet", + proposal, + duration, +): + return await subtensor.sign_and_send_extrinsic( + call=await subtensor.substrate.compose_call( + call_module="Triumvirate", + call_function="propose", + call_params={ + "proposal": proposal, + "length_bound": len(proposal.data), + "duration": duration, + }, + ), + wallet=wallet, + wait_for_finalization=True, + wait_for_inclusion=True, + ) + + +def vote( + subtensor: "SubtensorApi", + wallet: "Wallet", + hotkey, + proposal, + index, + approve, +): + return subtensor.sign_and_send_extrinsic( + subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="vote", + call_params={ + "approve": approve, + "hotkey": hotkey, + "index": index, + "proposal": proposal, + }, + ), + wallet, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + +async def async_vote( + subtensor: "SubtensorApi", + wallet: "Wallet", + hotkey, + proposal, + index, + approve, +): + return await subtensor.sign_and_send_extrinsic( + call=await subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="vote", + call_params={ + "approve": approve, + "hotkey": hotkey, + "index": index, + "proposal": proposal, + }, + ), + wallet=wallet, + wait_for_inclusion=True, + wait_for_finalization=True, + ) diff --git a/tests/e2e_tests/utils/e2e_test_utils.py b/tests/e2e_tests/utils/e2e_test_utils.py new file mode 100644 index 0000000000..f302662281 --- /dev/null +++ b/tests/e2e_tests/utils/e2e_test_utils.py @@ -0,0 +1,294 @@ +import asyncio +import os +import shutil +import subprocess +import sys + +from bittensor_wallet import Keypair, Wallet + +from bittensor.core.subtensor_api import SubtensorApi +from bittensor.utils.btlogging import logging + +template_path = os.getcwd() + "/neurons/" +templates_repo = "templates repository" + + +def setup_wallet(uri: str) -> tuple[Keypair, Wallet]: + """ + Sets up a wallet using the provided URI. + + This function creates a keypair from the given URI and initializes a wallet + at a temporary path. It sets the coldkey, coldkeypub, and hotkey for the wallet + using the generated keypair. + + Side Effects: + - Creates a wallet in a temporary directory. + - Sets keys in the wallet without encryption and with overwriting enabled. + """ + keypair = Keypair.create_from_uri(uri) + wallet_path = f"/tmp/btcli-e2e-wallet-{uri.strip('/')}" + wallet = Wallet(path=wallet_path) + wallet.set_coldkey(keypair=keypair, encrypt=False, overwrite=True) + wallet.set_coldkeypub(keypair=keypair, encrypt=False, overwrite=True) + wallet.set_hotkey(keypair=keypair, encrypt=False, overwrite=True) + return keypair, wallet + + +def clone_or_update_templates(specific_commit=None): + """ + Clones or updates the Bittensor subnet template repository. + + This function clones the Bittensor subnet template repository if it does not + already exist in the specified installation directory. If the repository already + exists, it updates it by pulling the latest changes. Optionally, it can check out + a specific commit if the `specific_commit` variable is set. + """ + install_dir = template_path + repo_mapping = { + templates_repo: "https://github.com/opentensor/subnet-template.git", + } + + cwd = os.getcwd() + + os.makedirs(install_dir, exist_ok=True) + os.chdir(install_dir) + + for repo, git_link in repo_mapping.items(): + print(os.path.abspath(repo)) + if not os.path.exists(repo): + print(f"\033[94mCloning {repo}...\033[0m") + subprocess.run(["git", "clone", git_link, repo], check=True) + else: + print(f"\033[94mUpdating {repo}...\033[0m") + os.chdir(repo) + subprocess.run(["git", "pull"], check=True) + os.chdir("..") + + # For pulling specific commit versions of repo + if specific_commit: + os.chdir(templates_repo) + print( + f"\033[94mChecking out commit {specific_commit} in {templates_repo}...\033[0m" + ) + subprocess.run(["git", "checkout", specific_commit], check=True) + os.chdir("..") + + os.chdir(cwd) + + return install_dir + templates_repo + + +def uninstall_templates(install_dir): + # Delete everything in directory + shutil.rmtree(install_dir) + + +class Templates: + class Miner: + def __init__(self, dir, wallet, netuid): + self.dir = dir + self.wallet = wallet + self.netuid = netuid + self.process = None + + self.started = asyncio.Event() + + async def __aenter__(self): + env = os.environ.copy() + env["BT_LOGGING_INFO"] = "1" + self.process = await asyncio.create_subprocess_exec( + sys.executable, + f"{self.dir}/miner.py", + "--netuid", + str(self.netuid), + "--subtensor.network", + "local", + "--subtensor.chain_endpoint", + "ws://localhost:9944", + "--wallet.path", + self.wallet.path, + "--wallet.name", + self.wallet.name, + "--wallet.hotkey", + "default", + env=env, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + + self.__reader_task = asyncio.create_task(self._reader()) + + try: + await asyncio.wait_for(self.started.wait(), 60) + except asyncio.TimeoutError: + self.process.kill() + await self.process.wait() + raise RuntimeError("Miner failed to start within timeout") + + return self + + async def __aexit__(self, exc_type, exc_value, traceback): + self.process.terminate() + self.__reader_task.cancel() + + await self.process.wait() + + async def _reader(self): + async for line in self.process.stdout: + try: + logging.console.info( + f"[green]MINER LOG: {line.split(b'|')[-1].strip().decode()}[/blue]" + ) + except Exception: + # skipp empty lines + pass + + if b"Starting main loop" in line: + self.started.set() + + class Validator: + def __init__(self, dir, wallet, netuid): + self.dir = dir + self.wallet = wallet + self.netuid = netuid + self.process = None + + self.started = asyncio.Event() + self.set_weights = asyncio.Event() + + async def __aenter__(self): + env = os.environ.copy() + env["BT_LOGGING_INFO"] = "1" + self.process = await asyncio.create_subprocess_exec( + sys.executable, + f"{self.dir}/validator.py", + "--netuid", + str(self.netuid), + "--subtensor.network", + "local", + "--subtensor.chain_endpoint", + "ws://localhost:9944", + "--wallet.path", + self.wallet.path, + "--wallet.name", + self.wallet.name, + "--wallet.hotkey", + "default", + env=env, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + + self.__reader_task = asyncio.create_task(self._reader()) + + try: + await asyncio.wait_for(self.started.wait(), 60) + except asyncio.TimeoutError: + self.process.kill() + await self.process.wait() + raise RuntimeError("Validator failed to start within timeout") + + return self + + async def __aexit__(self, exc_type, exc_value, traceback): + self.process.terminate() + self.__reader_task.cancel() + + await self.process.wait() + + async def _reader(self): + async for line in self.process.stdout: + try: + logging.console.info( + f"[orange]VALIDATOR LOG: {line.split(b'|')[-1].strip().decode()}[/orange]" + ) + except Exception: + # skipp empty lines + pass + + if b"Starting validator loop." in line: + logging.console.info("Validator started.") + self.started.set() + elif b"Successfully set weights and Finalized." in line: + logging.console.info("Validator is setting weights.") + self.set_weights.set() + + def __init__(self): + self.dir = clone_or_update_templates() + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + uninstall_templates(self.dir) + + def miner(self, wallet, netuid): + return self.Miner(self.dir, wallet, netuid) + + def validator(self, wallet, netuid): + return self.Validator(self.dir, wallet, netuid) + + +def wait_to_start_call( + subtensor: SubtensorApi, + subnet_owner_wallet: "Wallet", + netuid: int, + in_blocks: int = 10, +): + """Waits for a certain number of blocks before making a start call.""" + if subtensor.chain.is_fast_blocks() is False: + in_blocks = 5 + logging.console.info( + f"Waiting for [blue]{in_blocks}[/blue] blocks before [red]start call[/red]. " + f"Current block: [blue]{subtensor.block}[/blue]." + ) + + # make sure subnet isn't active + assert subtensor.subnets.is_subnet_active(netuid) is False, ( + "Subnet is already active." + ) + + # make sure we passed start_call limit + subtensor.wait_for_block(subtensor.block + in_blocks + 1) + status, message = subtensor.extrinsics.start_call( + wallet=subnet_owner_wallet, + netuid=netuid, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert status, message + # make sure subnet is active + assert subtensor.subnets.is_subnet_active(netuid), ( + "Subnet did not activated after start call." + ) + + return True + + +async def async_wait_to_start_call( + subtensor: "SubtensorApi", + subnet_owner_wallet: "Wallet", + netuid: int, + in_blocks: int = 10, +): + """Waits for a certain number of blocks before making a start call.""" + if await subtensor.chain.is_fast_blocks() is False: + in_blocks = 5 + + current_block = await subtensor.block + + logging.console.info( + f"Waiting for [blue]{in_blocks}[/blue] blocks before [red]start call[/red]. " + f"Current block: [blue]{current_block}[/blue]." + ) + + # make sure we passed start_call limit + await subtensor.wait_for_block(current_block + in_blocks + 1) + status, message = await subtensor.extrinsics.start_call( + wallet=subnet_owner_wallet, + netuid=netuid, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert status, message + return True diff --git a/tests/helpers/__init__.py b/tests/helpers/__init__.py new file mode 100644 index 0000000000..9097d72d70 --- /dev/null +++ b/tests/helpers/__init__.py @@ -0,0 +1,16 @@ +import os +from .helpers import ( # noqa: F401 + CloseInValue, + __mock_wallet_factory__, +) +from bittensor_wallet.mock.wallet_mock import ( # noqa: F401 + get_mock_coldkey, + get_mock_hotkey, + get_mock_keypair, + get_mock_wallet, +) + + +def is_running_in_circleci(): + """Checks that tests are running in the app.circleci.com environment.""" + return os.getenv("CIRCLECI") == "true" diff --git a/tests/helpers/helpers.py b/tests/helpers/helpers.py new file mode 100644 index 0000000000..6f94196273 --- /dev/null +++ b/tests/helpers/helpers.py @@ -0,0 +1,235 @@ +import asyncio +import itertools +import json +import time +from collections import deque +from typing import Optional, Union +from pathlib import Path + +from bittensor_wallet.mock.wallet_mock import MockWallet as _MockWallet +from bittensor_wallet.mock.wallet_mock import get_mock_coldkey +from bittensor_wallet.mock.wallet_mock import get_mock_hotkey +from bittensor_wallet.mock.wallet_mock import get_mock_wallet +from websockets.asyncio.client import ClientConnection, ClientProtocol +from websockets.uri import parse_uri + +from bittensor.core.chain_data import AxonInfo, NeuronInfo, PrometheusInfo +from bittensor.utils.balance import Balance +from tests.helpers.integration_websocket_data import WEBSOCKET_RESPONSES + +HELPERS_PATH = Path(__file__).parent + +with open(HELPERS_PATH / "integration_websocket_at_version.txt", "r") as f: + METADATA_AT_VERSION = f.read() + +with open(HELPERS_PATH / "integration_websocket_metadata.txt", "r") as f: + METADATA = f.read() + + +def __mock_wallet_factory__(*_, **__) -> _MockWallet: + """Returns a mock wallet object.""" + + mock_wallet = get_mock_wallet() + + return mock_wallet + + +class CloseInValue: + value: Union[float, int, Balance] + tolerance: Union[float, int, Balance] + + def __init__( + self, + value: Union[float, int, Balance], + tolerance: Union[float, int, Balance] = 0.0, + ) -> None: + self.value = value + self.tolerance = tolerance + + def __eq__(self, __o: Union[float, int, Balance]) -> bool: + # True if __o \in [value - tolerance, value + tolerance] + # or if value \in [__o - tolerance, __o + tolerance] + return ( + (self.value - self.tolerance) <= __o <= (self.value + self.tolerance) + ) or ((__o - self.tolerance) <= self.value <= (__o + self.tolerance)) + + def __str__(self) -> str: + return f"CloseInValue" + + def __repr__(self) -> str: + return self.__str__() + + +class ApproxBalance(CloseInValue, Balance): + def __init__( + self, + balance: Union[float, int], + tolerance: Union[float, int] = 0.1, + ): + super().__init__( + Balance(balance), + Balance(tolerance), + ) + + @property + def rao(self): + return self.value.rao + + +def assert_submit_signed_extrinsic( + substrate, + keypair, + call_module, + call_function, + call_params: Optional[dict] = None, + era: Optional[dict] = None, + nonce: Optional[int] = None, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +): + substrate.compose_call.assert_called_with( + call_module, + call_function, + call_params, + ) + + extrinsic = { + "call": substrate.compose_call.return_value, + "keypair": keypair, + } + + if era: + extrinsic["era"] = era + + if nonce: + extrinsic["nonce"] = nonce + + substrate.create_signed_extrinsic.assert_called_with( + **extrinsic, + ) + + substrate.submit_extrinsic.assert_called_with( + substrate.create_signed_extrinsic.return_value, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + +def get_mock_neuron(**kwargs) -> NeuronInfo: + """ + Returns a mock neuron with the given kwargs overriding the default values. + """ + + mock_neuron_d = dict( + { + "netuid": -1, # mock netuid + "axon_info": AxonInfo( + version=1, + ip="0.0.0.0", + port=0, + ip_type=0, + hotkey=get_mock_hotkey(), + coldkey=get_mock_coldkey(), + protocol=0, + placeholder1=0, + placeholder2=0, + ), + "prometheus_info": PrometheusInfo( + block=0, version=1, ip=0, port=0, ip_type=0 + ), + "validator_permit": True, + "uid": 1, + "hotkey": "some_hotkey", + "coldkey": "some_coldkey", + "active": 0, + "last_update": 0, + "stake": {"some_coldkey": 1e12}, + "total_stake": 1e12, + "rank": 0.0, + "trust": 0.0, + "consensus": 0.0, + "validator_trust": 0.0, + "incentive": 0.0, + "dividends": 0.0, + "emission": 0.0, + "bonds": [], + "weights": [], + "stake_dict": {}, + "pruning_score": 0.0, + "is_null": False, + } + ) + + mock_neuron_d.update(kwargs) # update with kwargs + + if kwargs.get("stake") is None and kwargs.get("coldkey") is not None: + mock_neuron_d["stake"] = {kwargs.get("coldkey"): 1e12} + + if kwargs.get("total_stake") is None: + mock_neuron_d["total_stake"] = sum(mock_neuron_d["stake"].values()) + + mock_neuron = NeuronInfo._neuron_dict_to_namespace(mock_neuron_d) + + return mock_neuron + + +def get_mock_neuron_by_uid(uid: int, **kwargs) -> NeuronInfo: + return get_mock_neuron( + uid=uid, hotkey=get_mock_hotkey(uid), coldkey=get_mock_coldkey(uid), **kwargs + ) + + +class FakeWebsocket(ClientConnection): + close_code = None + + def __init__(self, *args, seed, **kwargs): + protocol = ClientProtocol(parse_uri("ws://127.0.0.1:9945")) + super().__init__(protocol=protocol, **kwargs) + self.seed = seed + self.received = deque() + self._lock = asyncio.Lock() + + def send(self, payload: str, *args, **kwargs): + received = json.loads(payload) + id_ = received.pop("id") + self.received.append((received, id_)) + + def recv(self, *args, **kwargs): + while len(self.received) == 0: + time.sleep(0.1) + item, _id = self.received.pop() + try: + if item["method"] == "state_getMetadata": + response = {"jsonrpc": "2.0", "id": _id, "result": METADATA} + elif item[ + "method" + ] == "state_call" and "Metadata_metadata_at_version" in json.dumps( + item["params"] + ): + response = {"jsonrpc": "2.0", "id": _id, "result": METADATA_AT_VERSION} + else: + response = WEBSOCKET_RESPONSES[self.seed][item["method"]][ + json.dumps(item["params"]) + ] + if isinstance(response, itertools.cycle): + # Allows us to cycle through different responses for the same method/params combo + response = next(response) + response["id"] = _id + return json.dumps(response) + except (KeyError, TypeError): + print("ERROR", self.seed, item["method"], item["params"]) + raise + + def close(self, *args, **kwargs): + pass + + +class FakeConnectContextManager: + def __init__(self, seed): + self.seed = seed + + def __enter__(self): + return FakeWebsocket(seed=self.seed) + + def __exit__(self, exc_type, exc, tb): + pass diff --git a/tests/helpers/integration_websocket_at_version.txt b/tests/helpers/integration_websocket_at_version.txt new file mode 100644 index 0000000000..003c3dc1a7 --- /dev/null +++ b/tests/helpers/integration_websocket_at_version.txt @@ -0,0 +1 @@ +0x01e28b0d006d6574610fe508000c1c73705f636f72651863727970746f2c4163636f756e7449643332000004000401205b75383b2033325d0000040000032000000008000800000503000c08306672616d655f73797374656d2c4163636f756e74496e666f08144e6f6e636501102c4163636f756e74446174610114001401146e6f6e63651001144e6f6e6365000124636f6e73756d657273100120526566436f756e7400012470726f766964657273100120526566436f756e7400012c73756666696369656e7473100120526566436f756e740001106461746114012c4163636f756e74446174610000100000050500140c3c70616c6c65745f62616c616e6365731474797065732c4163636f756e7444617461041c42616c616e63650118001001106672656518011c42616c616e6365000120726573657276656418011c42616c616e636500011866726f7a656e18011c42616c616e6365000114666c6167731c01284578747261466c61677300001800000506001c0c3c70616c6c65745f62616c616e636573147479706573284578747261466c61677300000400200110753132380000200000050700240000050000280c346672616d655f737570706f7274206469737061746368405065724469737061746368436c617373040454012c000c01186e6f726d616c2c01045400012c6f7065726174696f6e616c2c0104540001246d616e6461746f72792c01045400002c0c2873705f77656967687473247765696768745f76321857656967687400000801207265665f74696d6530010c75363400012870726f6f665f73697a6530010c753634000030000006180034083c7072696d69746976655f74797065731048323536000004000401205b75383b2033325d00003800000208003c102873705f72756e74696d651c67656e65726963186469676573741844696765737400000401106c6f677340013c5665633c4469676573744974656d3e000040000002440044102873705f72756e74696d651c67656e6572696318646967657374284469676573744974656d0001142850726552756e74696d650800480144436f6e73656e737573456e67696e654964000038011c5665633c75383e00060024436f6e73656e7375730800480144436f6e73656e737573456e67696e654964000038011c5665633c75383e000400105365616c0800480144436f6e73656e737573456e67696e654964000038011c5665633c75383e000500144f74686572040038011c5665633c75383e0000006452756e74696d65456e7669726f6e6d656e745570646174656400080000480000030400000008004c00000250005008306672616d655f73797374656d2c4576656e745265636f7264080445015404540134000c011470686173655501011450686173650001146576656e7454010445000118746f70696373b401185665633c543e00005408586e6f64655f73756274656e736f725f72756e74696d653052756e74696d654576656e740001581853797374656d04005801706672616d655f73797374656d3a3a4576656e743c52756e74696d653e0000001c4772616e64706104007c015470616c6c65745f6772616e6470613a3a4576656e740004002042616c616e63657304008c017c70616c6c65745f62616c616e6365733a3a4576656e743c52756e74696d653e000500485472616e73616374696f6e5061796d656e7404009401a870616c6c65745f7472616e73616374696f6e5f7061796d656e743a3a4576656e743c52756e74696d653e0006003c53756274656e736f724d6f64756c65040098018070616c6c65745f73756274656e736f723a3a4576656e743c52756e74696d653e0007002c547269756d7669726174650400c001fc70616c6c65745f636f6c6c6563746976653a3a4576656e743c52756e74696d652c2070616c6c65745f636f6c6c6563746976653a3a496e7374616e6365313e00080048547269756d7669726174654d656d626572730400c401fc70616c6c65745f6d656d626572736869703a3a4576656e743c52756e74696d652c2070616c6c65745f6d656d626572736869703a3a496e7374616e6365313e0009003453656e6174654d656d626572730400c801fc70616c6c65745f6d656d626572736869703a3a4576656e743c52756e74696d652c2070616c6c65745f6d656d626572736869703a3a496e7374616e6365323e000a001c5574696c6974790400cc015470616c6c65745f7574696c6974793a3a4576656e74000b00105375646f0400d0016c70616c6c65745f7375646f3a3a4576656e743c52756e74696d653e000c00204d756c74697369670400d4017c70616c6c65745f6d756c74697369673a3a4576656e743c52756e74696d653e000d0020507265696d6167650400dc017c70616c6c65745f707265696d6167653a3a4576656e743c52756e74696d653e000e00245363686564756c65720400e0018070616c6c65745f7363686564756c65723a3a4576656e743c52756e74696d653e000f001450726f78790400ec017070616c6c65745f70726f78793a3a4576656e743c52756e74696d653e0010002052656769737472790400f4017c70616c6c65745f72656769737472793a3a4576656e743c52756e74696d653e0011002c436f6d6d69746d656e74730400f8018870616c6c65745f636f6d6d69746d656e74733a3a4576656e743c52756e74696d653e0012002841646d696e5574696c730400fc018870616c6c65745f61646d696e5f7574696c733a3a4576656e743c52756e74696d653e00130020536166654d6f646504000101018070616c6c65745f736166655f6d6f64653a3a4576656e743c52756e74696d653e00140020457468657265756d04000901015870616c6c65745f657468657265756d3a3a4576656e740015000c45564d04003501016870616c6c65745f65766d3a3a4576656e743c52756e74696d653e0016001c4261736546656504003d01015870616c6c65745f626173655f6665653a3a4576656e74001900144472616e6404004d01017070616c6c65745f6472616e643a3a4576656e743c52756e74696d653e001a0000580c306672616d655f73797374656d1870616c6c6574144576656e7404045400011c4045787472696e7369635375636365737304013464697370617463685f696e666f5c01304469737061746368496e666f00000490416e2065787472696e73696320636f6d706c65746564207375636365737366756c6c792e3c45787472696e7369634661696c656408013864697370617463685f6572726f7268013444697370617463684572726f7200013464697370617463685f696e666f5c01304469737061746368496e666f00010450416e2065787472696e736963206661696c65642e2c436f64655570646174656400020450603a636f6465602077617320757064617465642e284e65774163636f756e7404011c6163636f756e74000130543a3a4163636f756e7449640003046841206e6577206163636f756e742077617320637265617465642e344b696c6c65644163636f756e7404011c6163636f756e74000130543a3a4163636f756e74496400040458416e206163636f756e7420776173207265617065642e2052656d61726b656408011873656e646572000130543a3a4163636f756e7449640001106861736834011c543a3a48617368000504704f6e206f6e2d636861696e2072656d61726b2068617070656e65642e4455706772616465417574686f72697a6564080124636f64655f6861736834011c543a3a48617368000134636865636b5f76657273696f6e240110626f6f6c00060468416e20757067726164652077617320617574686f72697a65642e04704576656e7420666f72207468652053797374656d2070616c6c65742e5c0c346672616d655f737570706f7274206469737061746368304469737061746368496e666f00000c01187765696768742c0118576569676874000114636c6173736001344469737061746368436c617373000120706179735f666565640110506179730000600c346672616d655f737570706f7274206469737061746368344469737061746368436c61737300010c184e6f726d616c0000002c4f7065726174696f6e616c000100244d616e6461746f727900020000640c346672616d655f737570706f727420646973706174636810506179730001080c596573000000084e6f0001000068082873705f72756e74696d653444697370617463684572726f72000138144f746865720000003043616e6e6f744c6f6f6b7570000100244261644f726967696e000200184d6f64756c6504006c012c4d6f64756c654572726f7200030044436f6e73756d657252656d61696e696e670004002c4e6f50726f76696465727300050040546f6f4d616e79436f6e73756d65727300060014546f6b656e0400700128546f6b656e4572726f720007002841726974686d65746963040074013c41726974686d657469634572726f72000800345472616e73616374696f6e616c04007801485472616e73616374696f6e616c4572726f7200090024457868617573746564000a0028436f7272757074696f6e000b002c556e617661696c61626c65000c0038526f6f744e6f74416c6c6f776564000d00006c082873705f72756e74696d652c4d6f64756c654572726f720000080114696e64657808010875380001146572726f7248018c5b75383b204d41585f4d4f44554c455f4552524f525f454e434f4445445f53495a455d000070082873705f72756e74696d6528546f6b656e4572726f720001284046756e6473556e617661696c61626c65000000304f6e6c7950726f76696465720001003042656c6f774d696e696d756d0002003043616e6e6f7443726561746500030030556e6b6e6f776e41737365740004001846726f7a656e0005002c556e737570706f727465640006004043616e6e6f74437265617465486f6c64000700344e6f74457870656e6461626c650008001c426c6f636b65640009000074083473705f61726974686d657469633c41726974686d657469634572726f7200010c24556e646572666c6f77000000204f766572666c6f77000100384469766973696f6e42795a65726f0002000078082873705f72756e74696d65485472616e73616374696f6e616c4572726f72000108304c696d6974526561636865640000001c4e6f4c61796572000100007c0c3870616c6c65745f6772616e6470611870616c6c6574144576656e7400010c384e6577417574686f726974696573040134617574686f726974795f736574800134417574686f726974794c6973740000048c4e657720617574686f726974792073657420686173206265656e206170706c6965642e185061757365640001049843757272656e7420617574686f726974792073657420686173206265656e207061757365642e1c526573756d65640002049c43757272656e7420617574686f726974792073657420686173206265656e20726573756d65642e047c54686520604576656e746020656e756d206f6620746869732070616c6c65748000000284008400000408881800880c5073705f636f6e73656e7375735f6772616e6470610c617070185075626c69630000040004013c656432353531393a3a5075626c696300008c0c3c70616c6c65745f62616c616e6365731870616c6c6574144576656e740804540004490001581c456e646f77656408011c6163636f756e74000130543a3a4163636f756e744964000130667265655f62616c616e6365180128543a3a42616c616e6365000004b8416e206163636f756e74207761732063726561746564207769746820736f6d6520667265652062616c616e63652e20447573744c6f737408011c6163636f756e74000130543a3a4163636f756e744964000118616d6f756e74180128543a3a42616c616e63650001083d01416e206163636f756e74207761732072656d6f7665642077686f73652062616c616e636520776173206e6f6e2d7a65726f206275742062656c6f77204578697374656e7469616c4465706f7369742c78726573756c74696e6720696e20616e206f75747269676874206c6f73732e205472616e736665720c011066726f6d000130543a3a4163636f756e744964000108746f000130543a3a4163636f756e744964000118616d6f756e74180128543a3a42616c616e63650002044c5472616e73666572207375636365656465642e2842616c616e636553657408010c77686f000130543a3a4163636f756e74496400011066726565180128543a3a42616c616e636500030468412062616c616e6365207761732073657420627920726f6f742e20526573657276656408010c77686f000130543a3a4163636f756e744964000118616d6f756e74180128543a3a42616c616e6365000404e0536f6d652062616c616e63652077617320726573657276656420286d6f7665642066726f6d206672656520746f207265736572766564292e28556e726573657276656408010c77686f000130543a3a4163636f756e744964000118616d6f756e74180128543a3a42616c616e6365000504e8536f6d652062616c616e63652077617320756e726573657276656420286d6f7665642066726f6d20726573657276656420746f2066726565292e4852657365727665526570617472696174656410011066726f6d000130543a3a4163636f756e744964000108746f000130543a3a4163636f756e744964000118616d6f756e74180128543a3a42616c616e636500014864657374696e6174696f6e5f7374617475739001185374617475730006084d01536f6d652062616c616e636520776173206d6f7665642066726f6d207468652072657365727665206f6620746865206669727374206163636f756e7420746f20746865207365636f6e64206163636f756e742ed846696e616c20617267756d656e7420696e64696361746573207468652064657374696e6174696f6e2062616c616e636520747970652e1c4465706f73697408010c77686f000130543a3a4163636f756e744964000118616d6f756e74180128543a3a42616c616e6365000704d8536f6d6520616d6f756e7420776173206465706f73697465642028652e672e20666f72207472616e73616374696f6e2066656573292e20576974686472617708010c77686f000130543a3a4163636f756e744964000118616d6f756e74180128543a3a42616c616e63650008041d01536f6d6520616d6f756e74207761732077697468647261776e2066726f6d20746865206163636f756e742028652e672e20666f72207472616e73616374696f6e2066656573292e1c536c617368656408010c77686f000130543a3a4163636f756e744964000118616d6f756e74180128543a3a42616c616e63650009040101536f6d6520616d6f756e74207761732072656d6f7665642066726f6d20746865206163636f756e742028652e672e20666f72206d69736265686176696f72292e184d696e74656408010c77686f000130543a3a4163636f756e744964000118616d6f756e74180128543a3a42616c616e6365000a049c536f6d6520616d6f756e7420776173206d696e74656420696e746f20616e206163636f756e742e184275726e656408010c77686f000130543a3a4163636f756e744964000118616d6f756e74180128543a3a42616c616e6365000b049c536f6d6520616d6f756e7420776173206275726e65642066726f6d20616e206163636f756e742e2453757370656e64656408010c77686f000130543a3a4163636f756e744964000118616d6f756e74180128543a3a42616c616e6365000c041501536f6d6520616d6f756e74207761732073757370656e6465642066726f6d20616e206163636f756e74202869742063616e20626520726573746f726564206c61746572292e20526573746f72656408010c77686f000130543a3a4163636f756e744964000118616d6f756e74180128543a3a42616c616e6365000d04a4536f6d6520616d6f756e742077617320726573746f72656420696e746f20616e206163636f756e742e20557067726164656404010c77686f000130543a3a4163636f756e744964000e0460416e206163636f756e74207761732075706772616465642e18497373756564040118616d6f756e74180128543a3a42616c616e6365000f042d01546f74616c2069737375616e63652077617320696e637265617365642062792060616d6f756e74602c206372656174696e6720612063726564697420746f2062652062616c616e6365642e2452657363696e646564040118616d6f756e74180128543a3a42616c616e63650010042501546f74616c2069737375616e636520776173206465637265617365642062792060616d6f756e74602c206372656174696e672061206465627420746f2062652062616c616e6365642e184c6f636b656408010c77686f000130543a3a4163636f756e744964000118616d6f756e74180128543a3a42616c616e636500110460536f6d652062616c616e636520776173206c6f636b65642e20556e6c6f636b656408010c77686f000130543a3a4163636f756e744964000118616d6f756e74180128543a3a42616c616e636500120468536f6d652062616c616e63652077617320756e6c6f636b65642e1846726f7a656e08010c77686f000130543a3a4163636f756e744964000118616d6f756e74180128543a3a42616c616e636500130460536f6d652062616c616e6365207761732066726f7a656e2e1854686177656408010c77686f000130543a3a4163636f756e744964000118616d6f756e74180128543a3a42616c616e636500140460536f6d652062616c616e636520776173207468617765642e4c546f74616c49737375616e6365466f7263656408010c6f6c64180128543a3a42616c616e636500010c6e6577180128543a3a42616c616e6365001504ac5468652060546f74616c49737375616e6365602077617320666f72636566756c6c79206368616e6765642e047c54686520604576656e746020656e756d206f6620746869732070616c6c65749014346672616d655f737570706f72741874726169747318746f6b656e73106d6973633442616c616e6365537461747573000108104672656500000020526573657276656400010000940c6870616c6c65745f7472616e73616374696f6e5f7061796d656e741870616c6c6574144576656e74040454000104485472616e73616374696f6e466565506169640c010c77686f000130543a3a4163636f756e74496400012861637475616c5f66656518013042616c616e63654f663c543e00010c74697018013042616c616e63654f663c543e000008590141207472616e73616374696f6e20666565206061637475616c5f666565602c206f662077686963682060746970602077617320616464656420746f20746865206d696e696d756d20696e636c7573696f6e206665652c5c686173206265656e2070616964206279206077686f602e047c54686520604576656e746020656e756d206f6620746869732070616c6c6574980c4070616c6c65745f73756274656e736f721870616c6c6574144576656e7404045400016101304e6574776f726b416464656408009c010c75313600009c010c7531360000045c61206e6577206e6574776f726b2069732061646465642e384e6574776f726b52656d6f76656404009c010c7531360001045461206e6574776f726b2069732072656d6f7665642e285374616b6541646465641400000130543a3a4163636f756e7449640000000130543a3a4163636f756e744964000018010c753634000018010c75363400009c010c75313600020459017374616b6520686173206265656e207472616e736665727265642066726f6d20746865206120636f6c646b6579206163636f756e74206f6e746f2074686520686f746b6579207374616b696e67206163636f756e742e305374616b6552656d6f7665641400000130543a3a4163636f756e7449640000000130543a3a4163636f756e744964000018010c753634000018010c75363400009c010c75313600030441017374616b6520686173206265656e2072656d6f7665642066726f6d2074686520686f746b6579207374616b696e67206163636f756e74206f6e746f2074686520636f6c646b6579206163636f756e742e285374616b654d6f7665641800000130543a3a4163636f756e7449640000000130543a3a4163636f756e74496400009c010c7531360000000130543a3a4163636f756e74496400009c010c753136000018010c753634000404c1017374616b6520686173206265656e206d6f7665642066726f6d206f726967696e2028686f746b65792c207375626e65742049442920746f2064657374696e6174696f6e2028686f746b65792c207375626e657420494429206f66207468697320616d6f756e742028696e2054414f292e285765696768747353657408009c010c75313600009c010c753136000504e4612063616c6c6572207375636365737366756c6c7920736574732074686569722077656967687473206f6e2061207375626e6574776f726b2e404e6575726f6e526567697374657265640c009c010c75313600009c010c7531360000000130543a3a4163636f756e744964000604d861206e6577206e6575726f6e206163636f756e7420686173206265656e207265676973746572656420746f2074686520636861696e2e5442756c6b4e6575726f6e735265676973746572656408009c010c75313600009c010c753136000704c06d756c7469706c6520756964732068617665206265656e20636f6e63757272656e746c7920726567697374657265642e3c42756c6b42616c616e63657353657408009c010c75313600009c010c7531360008044c4649584d453a204e6f74207573656420796574444d6178416c6c6f7765645569647353657408009c010c75313600009c010c753136000904bc6d617820616c6c6f776564207569647320686173206265656e2073657420666f722061207375626e6574776f726b2e444d61785765696768744c696d697453657408009c010c75313600009c010c753136000a04cc746865206d617820776569676874206c696d697420686173206265656e2073657420666f722061207375626e6574776f726b2e34446966666963756c747953657408009c010c753136000018010c753634000b04a474686520646966666963756c747920686173206265656e2073657420666f722061207375626e65742e5441646a7573746d656e74496e74657276616c53657408009c010c75313600009c010c753136000c04b07468652061646a7573746d656e7420696e74657276616c2069732073657420666f722061207375626e65742e68526567697374726174696f6e506572496e74657276616c53657408009c010c75313600009c010c753136000d04b8726567697374726174696f6e2070657220696e74657276616c2069732073657420666f722061207375626e65742e6c4d6178526567697374726174696f6e73506572426c6f636b53657408009c010c75313600009c010c753136000e048c776520736574206d617820726567697374726174696f6e732070657220626c6f636b2e4441637469766974794375746f666653657408009c010c75313600009c010c753136000f049c616e206163746976697479206375746f66662069732073657420666f722061207375626e65742e1852686f53657408009c010c75313600009c010c7531360010044452686f2076616c7565206973207365742e204b6170706153657408009c010c75313600009c010c753136001104684b617070612069732073657420666f722061207375626e65742e4c4d696e416c6c6f77656457656967687453657408009c010c75313600009c010c753136001204ac6d696e696d756d20616c6c6f776564207765696768742069732073657420666f722061207375626e65742e5056616c696461746f725072756e654c656e53657408009c010c753136000018010c753634001304a87468652076616c696461746f72207072756e696e67206c656e67746820686173206265656e207365742e485363616c696e674c6177506f77657253657408009c010c75313600009c010c753136001404c0746865207363616c696e67206c617720706f77657220686173206265656e2073657420666f722061207375626e65742e5857656967687473536574526174654c696d697453657408009c010c753136000018010c753634001504c477656967687473207365742072617465206c696d697420686173206265656e2073657420666f722061207375626e65742e44496d6d756e697479506572696f6453657408009c010c75313600009c010c75313600160490696d6d756e69747920706572696f642069732073657420666f722061207375626e65742e54426f6e64734d6f76696e674176657261676553657408009c010c753136000018010c753634001704a4626f6e6473206d6f76696e6720617665726167652069732073657420666f722061207375626e65742e3c426f6e647350656e616c747953657408009c010c75313600009c010c75313600180488626f6e64732070656e616c74792069732073657420666f722061207375626e65742e5c4d6178416c6c6f77656456616c696461746f727353657408009c010c75313600009c010c753136001904e473657474696e6720746865206d6178206e756d626572206f6620616c6c6f7765642076616c696461746f7273206f6e2061207375626e65742e2841786f6e53657276656408009c010c7531360000000130543a3a4163636f756e744964001a04d07468652061786f6e2073657276657220696e666f726d6174696f6e20697320616464656420746f20746865206e6574776f726b2e4050726f6d65746865757353657276656408009c010c7531360000000130543a3a4163636f756e744964001b04e87468652070726f6d6574686575732073657276657220696e666f726d6174696f6e20697320616464656420746f20746865206e6574776f726b2e44456d697373696f6e56616c756573536574001c04a0656d697373696f6e20726174696f7320666f7220616c6c206e6574776f726b73206973207365742e3444656c656761746541646465640c00000130543a3a4163636f756e7449640000000130543a3a4163636f756e74496400009c010c753136001d047c6120686f746b657920686173206265636f6d6520612064656c65676174652e3844656661756c7454616b6553657404009c010c753136001e04607468652064656661756c742074616b65206973207365742e505765696768747356657273696f6e4b657953657408009c010c753136000018010c753634001f04a4776569676874732076657273696f6e206b65792069732073657420666f722061206e6574776f726b2e404d696e446966666963756c747953657408009c010c753136000018010c7536340020049073657474696e67206d696e20646966666963756c7479206f6e2061206e6574776f726b2e404d6178446966666963756c747953657408009c010c753136000018010c7536340021049073657474696e67206d617820646966666963756c7479206f6e2061206e6574776f726b2e4c53657276696e67526174654c696d697453657408009c010c753136000018010c753634002204a873657474696e67207468652070726f6d6574686575732073657276696e672072617465206c696d69742e1c4275726e53657408009c010c753136000018010c7536340023046873657474696e67206275726e206f6e2061206e6574776f726b2e284d61784275726e53657408009c010c753136000018010c7536340024047873657474696e67206d6178206275726e206f6e2061206e6574776f726b2e284d696e4275726e53657408009c010c753136000018010c7536340025047873657474696e67206d696e206275726e206f6e2061206e6574776f726b2e385478526174654c696d6974536574040018010c7536340026048c73657474696e6720746865207472616e73616374696f6e2072617465206c696d69742e68547844656c656761746554616b65526174654c696d6974536574040018010c753634002704c473657474696e67207468652064656c65676174652074616b65207472616e73616374696f6e2072617465206c696d69742e6854784368696c644b657954616b65526174654c696d6974536574040018010c753634002804c473657474696e6720746865206368696c646b65792074616b65207472616e73616374696f6e2072617465206c696d69742e484d696e4368696c644b657954616b6553657404009c010c753136002904646d696e696d756d206368696c646b65792074616b6520736574484d61784368696c644b657954616b6553657404009c010c753136002a04646d6178696d756d206368696c646b65792074616b65207365743c4368696c644b657954616b655365740800000130543a3a4163636f756e74496400009c010c753136002b04446368696c646b65792074616b65207365741453756469640400a001384469737061746368526573756c74002c045061207375646f2063616c6c20697320646f6e652e4c526567697374726174696f6e416c6c6f77656408009c010c7531360000240110626f6f6c002d04c0726567697374726174696f6e20697320616c6c6f7765642f646973616c6c6f77656420666f722061207375626e65742e58506f77526567697374726174696f6e416c6c6f77656408009c010c7531360000240110626f6f6c002e04d0504f5720726567697374726174696f6e20697320616c6c6f7765642f646973616c6c6f77656420666f722061207375626e65742e2054656d706f53657408009c010c75313600009c010c753136002f046873657474696e672074656d706f206f6e2061206e6574776f726b7452414f52656379636c6564466f72526567697374726174696f6e53657408009c010c753136000018010c753634003004a873657474696e67207468652052414f2072656379636c656420666f7220726567697374726174696f6e2e445374616b655468726573686f6c64536574040018010c753634003104bc6d696e207374616b652069732073657420666f722076616c696461746f727320746f2073657420776569676874732e7453656e61746552657175697265645374616b6550657263656e74536574040018010c753634003204090173657474696e6720746865206d696e696d756d207265717569726564207374616b6520616d6f756e7420666f722073656e61746520726567697374726174696f6e2e4841646a7573746d656e74416c70686153657408009c010c753136000018010c753634003304a473657474696e67207468652061646a7573746d656e7420616c706861206f6e2061207375626e65742e184661756365740800000130543a3a4163636f756e744964000018010c75363400340494746865206661756365742069742063616c6c6564206f6e207468652074657374206e65742e445375626e65744f776e657243757453657404009c010c75313600350470746865207375626e6574206f776e657220637574206973207365742e4c4e6574776f726b526174654c696d6974536574040018010c7536340036049c746865206e6574776f726b206372656174696f6e2072617465206c696d6974206973207365742e604e6574776f726b496d6d756e697479506572696f64536574040018010c7536340037048c746865206e6574776f726b20696d6d756e69747920706572696f64206973207365742e544e6574776f726b4d696e4c6f636b436f7374536574040018010c753634003804a0746865206e6574776f726b206d696e696d756d206c6f636b696e6720636f7374206973207365742e385375626e65744c696d697453657404009c010c75313600390490746865206d6178696d756d206e756d626572206f66207375626e657473206973207365748c4e6574776f726b4c6f636b436f7374526564756374696f6e496e74657276616c536574040018010c753634003a0478746865206c6f636b20636f737420726564756374696f6e206973207365743454616b654465637265617365640c00000130543a3a4163636f756e7449640000000130543a3a4163636f756e74496400009c010c753136003b04947468652074616b6520666f7220612064656c6567617465206973206465637265617365642e3454616b65496e637265617365640c00000130543a3a4163636f756e7449640000000130543a3a4163636f756e74496400009c010c753136003c04947468652074616b6520666f7220612064656c656761746520697320696e637265617365642e34486f746b6579537761707065640c011c636f6c646b6579000130543a3a4163636f756e7449640464746865206163636f756e74204944206f6620636f6c646b657901286f6c645f686f746b6579000130543a3a4163636f756e7449640470746865206163636f756e74204944206f66206f6c6420686f746b657901286e65775f686f746b6579000130543a3a4163636f756e7449640470746865206163636f756e74204944206f66206e657720686f746b65793d045474686520686f746b65792069732073776170706564484d617844656c656761746554616b6553657404009c010c753136003e04d86d6178696d756d2064656c65676174652074616b6520697320736574206279207375646f2f61646d696e207472616e73616374696f6e484d696e44656c656761746554616b6553657404009c010c753136003f04d86d696e696d756d2064656c65676174652074616b6520697320736574206279207375646f2f61646d696e207472616e73616374696f6e3853656e61746541646a75737465640801286f6c645f6d656d626572a801504f7074696f6e3c543a3a4163636f756e7449643e04bc746865206163636f756e74204944206f6620746865206f6c642073656e617465206d656d6265722c20696620616e7901286e65775f6d656d626572000130543a3a4163636f756e744964049c746865206163636f756e74204944206f6620746865206e65772073656e617465206d656d62657240048861206d656d626572206f66207468652073656e6174652069732061646a757374656438436f6c646b6579537761707065640c012c6f6c645f636f6c646b6579000130543a3a4163636f756e7449640474746865206163636f756e74204944206f66206f6c6420636f6c646b6579012c6e65775f636f6c646b6579000130543a3a4163636f756e7449640474746865206163636f756e74204944206f66206e657720636f6c646b65790124737761705f636f737418010c7536340434746865207377617020636f73744104684120636f6c646b657920686173206265656e2073776170706564b0416c6c42616c616e6365556e7374616b6564416e645472616e73666572726564546f4e6577436f6c646b65790c013c63757272656e745f636f6c646b6579000130543a3a4163636f756e7449640494546865206163636f756e74204944206f66207468652063757272656e7420636f6c646b6579012c6e65775f636f6c646b6579000130543a3a4163636f756e7449640484546865206163636f756e74204944206f6620746865206e657720636f6c646b65790134746f74616c5f62616c616e6365180185013c3c5420617320436f6e6669673e3a3a43757272656e63792061732066756e6769626c653a3a496e73706563743c3c54206173206672616d655f73797374656d3a3a0a436f6e6669673e3a3a4163636f756e7449642c3e3e3a3a42616c616e6365047c54686520746f74616c2062616c616e6365206f662074686520686f746b657942042901416c6c2062616c616e6365206f66206120686f746b657920686173206265656e20756e7374616b656420616e64207472616e7366657272656420746f2061206e657720636f6c646b657950436f6c646b6579537761705363686564756c656410012c6f6c645f636f6c646b6579000130543a3a4163636f756e7449640484546865206163636f756e74204944206f6620746865206f6c6420636f6c646b6579012c6e65775f636f6c646b6579000130543a3a4163636f756e7449640484546865206163636f756e74204944206f6620746865206e657720636f6c646b6579013c657865637574696f6e5f626c6f636b100144426c6f636b4e756d626572466f723c543e04a8546865206172626974726174696f6e20626c6f636b20666f722074686520636f6c646b657920737761700124737761705f636f737418010c7536340434546865207377617020636f73744304844120636f6c646b6579207377617020686173206265656e207363686564756c6564644172626974726174696f6e506572696f64457874656e64656404011c636f6c646b6579000130543a3a4163636f756e7449640474546865206163636f756e74204944206f662074686520636f6c646b65794404a0546865206172626974726174696f6e20706572696f6420686173206265656e20657874656e646564505365744368696c6472656e5363686564756c65641000000130543a3a4163636f756e74496400009c010c753136000018010c7536340000ac01605665633c287536342c20543a3a4163636f756e744964293e004504cc53657474696e67206f66206368696c6472656e206f66206120686f746b65792068617665206265656e207363686564756c65642c5365744368696c6472656e0c00000130543a3a4163636f756e74496400009c010c7531360000ac01605665633c287536342c20543a3a4163636f756e744964293e00460498546865206368696c6472656e206f66206120686f746b65792068617665206265656e20736574484e6574776f726b4d61785374616b6553657408009c010c753136000018010c75363400470498546865206e6574776f726b206d6178696d756d207374616b6520686173206265656e2073657440436861696e4964656e746974795365740400000130543a3a4163636f756e74496400480498546865206964656e74697479206f66206120636f6c646b657920686173206265656e20736574445375626e65744964656e7469747953657404009c010c75313600490494546865206964656e74697479206f662061207375626e657420686173206265656e20736574545375626e65744964656e7469747952656d6f76656404009c010c753136004a04a4546865206964656e74697479206f662061207375626e657420686173206265656e2072656d6f76656460446973736f6c76654e6574776f726b5363686564756c65640c011c6163636f756e74000130543a3a4163636f756e74496404d8546865206163636f756e74204944207363686564756c652074686520646973736f6c7665206e6574776f726b206578747269736e696301186e65747569649c010c75313604706e6574776f726b2049442077696c6c20626520646973736f6c766564013c657865637574696f6e5f626c6f636b100144426c6f636b4e756d626572466f723c543e048065787472696e73696320657865637574696f6e20626c6f636b206e756d6265724b049c4120646973736f6c7665206e6574776f726b2065787472696e736963207363686564756c65642e78436f6c646b6579537761705363686564756c654475726174696f6e5365740400100144426c6f636b4e756d626572466f723c543e004c04c8546865206475726174696f6e206f66207363686564756c6520636f6c646b6579207377617020686173206265656e2073657488446973736f6c76654e6574776f726b5363686564756c654475726174696f6e5365740400100144426c6f636b4e756d626572466f723c543e004d04b4546865206475726174696f6e206f6620646973736f6c7665206e6574776f726b20686173206265656e20736574504352563357656967687473436f6d6d69747465640c00000130543a3a4163636f756e74496400009c010c753136000034011048323536004e14e8436f6d6d69742d72657665616c20763320776569676874732068617665206265656e207375636365737366756c6c7920636f6d6d69747465642e00f42d202a2a77686f2a2a3a20546865206163636f756e74204944206f6620746865207573657220636f6d6d697474696e672074686520776569676874732e942d202a2a6e65747569642a2a3a20546865206e6574776f726b206964656e7469666965722efc2d202a2a636f6d6d69745f686173682a2a3a20546865206861736820726570726573656e74696e672074686520636f6d6d697474656420776569676874732e4057656967687473436f6d6d69747465640c00000130543a3a4163636f756e74496400009c010c753136000034011048323536004f14a4576569676874732068617665206265656e207375636365737366756c6c7920636f6d6d69747465642e00f42d202a2a77686f2a2a3a20546865206163636f756e74204944206f6620746865207573657220636f6d6d697474696e672074686520776569676874732e942d202a2a6e65747569642a2a3a20546865206e6574776f726b206964656e7469666965722efc2d202a2a636f6d6d69745f686173682a2a3a20546865206861736820726570726573656e74696e672074686520636f6d6d697474656420776569676874732e3c5765696768747352657665616c65640c00000130543a3a4163636f756e74496400009c010c753136000034011048323536005014a0576569676874732068617665206265656e207375636365737366756c6c792072657665616c65642e00f02d202a2a77686f2a2a3a20546865206163636f756e74204944206f662074686520757365722072657665616c696e672074686520776569676874732e942d202a2a6e65747569642a2a3a20546865206e6574776f726b206964656e7469666965722ed02d202a2a636f6d6d69745f686173682a2a3a205468652068617368206f66207468652072657665616c656420776569676874732e5057656967687473426174636852657665616c65640c00000130543a3a4163636f756e74496400009c010c7531360000b401245665633c483235363e005114b8576569676874732068617665206265656e207375636365737366756c6c792062617463682072657665616c65642e00f02d202a2a77686f2a2a3a20546865206163636f756e74204944206f662074686520757365722072657665616c696e672074686520776569676874732e942d202a2a6e65747569642a2a3a20546865206e6574776f726b206964656e7469666965722e41012d202a2a72657665616c65645f6861736865732a2a3a204120766563746f72206f662068617368657320726570726573656e74696e6720656163682072657665616c656420776569676874207365742e54426174636857656967687473436f6d706c657465640800b801445665633c436f6d706163743c7531363e3e0000000130543a3a4163636f756e744964005210d041206261746368206f66207765696768747320286f7220636f6d6d697473292068617665206265656e20666f7263652d7365742e0035012d202a2a6e6574756964732a2a3a20546865206e65747569647320746865736520776569676874732077657265207375636365737366756c6c79207365742f636f6d6d697474656420666f722ea82d202a2a77686f2a2a3a2054686520686f746b657920746861742073657420746869732062617463682e604261746368436f6d706c65746564576974684572726f7273005304c4412062617463682065787472696e73696320636f6d706c6574656420627574207769746820736f6d65206572726f72732e5442617463685765696768744974656d4661696c6564040068016473705f72756e74696d653a3a44697370617463684572726f7200540cb441207765696768742073657420616d6f6e672061206261746368206f662077656967687473206661696c65642e00ec2d202a2a6572726f722a2a3a20546865206469737061746368206572726f7220656d697474656420627920746865206661696c6564206974656d2e405374616b655472616e736665727265641800000130543a3a4163636f756e7449640000000130543a3a4163636f756e7449640000000130543a3a4163636f756e74496400009c010c75313600009c010c753136000018010c75363400550c29015374616b6520686173206265656e207472616e736665727265642066726f6d206f6e6520636f6c646b657920746f20616e6f74686572206f6e207468652073616d65207375626e65742e2c506172616d65746572733a6101286f726967696e5f636f6c646b65792c2064657374696e6174696f6e5f636f6c646b65792c20686f746b65792c206f726967696e5f6e65747569642c2064657374696e6174696f6e5f6e65747569642c20616d6f756e7429305374616b65537761707065641400000130543a3a4163636f756e7449640000000130543a3a4163636f756e74496400009c010c75313600009c010c753136000018010c7536340056104d015374616b6520686173206265656e20737761707065642066726f6d206f6e65207375626e657420746f20616e6f7468657220666f72207468652073616d6520636f6c646b65792d686f746b657920706169722e002c506172616d65746572733af028636f6c646b65792c20686f746b65792c206f726967696e5f6e65747569642c2064657374696e6174696f6e5f6e65747569642c20616d6f756e7429385472616e73666572546f67676c6508009c010c7531360000240110626f6f6c005710c84576656e742063616c6c6564207768656e207472616e7366657220697320746f67676c6564206f6e2061207375626e65742e002c506172616d65746572733a38286e65747569642c20626f6f6c29047c54686520604576656e746020656e756d206f6620746869732070616c6c65749c0000050400a00418526573756c7408045401a4044501680108084f6b0400a4000000000c4572720400680000010000a40000040000a804184f7074696f6e04045401000108104e6f6e6500000010536f6d650400000000010000ac000002b000b000000408180000b40000023400b8000002bc00bc0000069c00c00c4470616c6c65745f636f6c6c6563746976651870616c6c6574144576656e7408045400044900011c2050726f706f73656410011c6163636f756e74000130543a3a4163636f756e7449640494546865206163636f756e7420746861742070726f706f73656420746865206d6f74696f6e2e013870726f706f73616c5f696e64657810013450726f706f73616c496e646578046854686520696e646578206f66207468652070726f706f73616c2e013470726f706f73616c5f6861736834011c543a3a4861736804645468652068617368206f66207468652070726f706f73616c2e01247468726573686f6c6410012c4d656d626572436f756e7404a4546865207468726573686f6c64206f66206d656d62657220666f72207468652070726f706f73616c2e0008490141206d6f74696f6e2028676976656e20686173682920686173206265656e2070726f706f7365642028627920676976656e206163636f756e742920776974682061207468726573686f6c642028676976656e3c604d656d626572436f756e7460292e14566f74656414011c6163636f756e74000130543a3a4163636f756e744964045c546865206163636f756e74207468617420766f7465642e013470726f706f73616c5f6861736834011c543a3a4861736804645468652068617368206f66207468652070726f706f73616c2e0114766f746564240110626f6f6c04785768657468657220746865206163636f756e7420766f746564206179652e010c79657310012c4d656d626572436f756e740460546865206e756d626572206f662079657320766f7465732e01086e6f10012c4d656d626572436f756e74045c546865206e756d626572206f66206e6f20766f7465732e0108050141206d6f74696f6e2028676976656e20686173682920686173206265656e20766f746564206f6e20627920676976656e206163636f756e742c206c656176696e671501612074616c6c79202879657320766f74657320616e64206e6f20766f74657320676976656e20726573706563746976656c7920617320604d656d626572436f756e7460292e20417070726f76656404013470726f706f73616c5f6861736834011c543a3a4861736804645468652068617368206f66207468652070726f706f73616c2e0204c041206d6f74696f6e2077617320617070726f76656420627920746865207265717569726564207468726573686f6c642e2c446973617070726f76656404013470726f706f73616c5f6861736834011c543a3a4861736804645468652068617368206f66207468652070726f706f73616c2e0304d041206d6f74696f6e20776173206e6f7420617070726f76656420627920746865207265717569726564207468726573686f6c642e20457865637574656408013470726f706f73616c5f6861736834011c543a3a4861736804645468652068617368206f66207468652070726f706f73616c2e0118726573756c74a001384469737061746368526573756c74047054686520726573756c74206f662074686520657865637574696f6e2e0404210141206d6f74696f6e207761732065786563757465643b20726573756c742077696c6c20626520604f6b602069662069742072657475726e656420776974686f7574206572726f722e384d656d626572457865637574656408013470726f706f73616c5f6861736834011c543a3a4861736804645468652068617368206f66207468652070726f706f73616c2e0118726573756c74a001384469737061746368526573756c74047054686520726573756c74206f662074686520657865637574696f6e2e05044901412073696e676c65206d656d6265722064696420736f6d6520616374696f6e3b20726573756c742077696c6c20626520604f6b602069662069742072657475726e656420776974686f7574206572726f722e18436c6f7365640c013470726f706f73616c5f6861736834011c543a3a4861736804645468652068617368206f66207468652070726f706f73616c2e010c79657310012c4d656d626572436f756e74048857686574686572207468652070726f706f73616c2077617320617070726f7665642e01086e6f10012c4d656d626572436f756e74048857686574686572207468652070726f706f73616c207761732072656a65637465642e06045501412070726f706f73616c2077617320636c6f736564206265636175736520697473207468726573686f6c64207761732072656163686564206f7220616674657220697473206475726174696f6e207761732075702e047c54686520604576656e746020656e756d206f6620746869732070616c6c6574c40c4470616c6c65745f6d656d626572736869701870616c6c6574144576656e740804540004490001182c4d656d6265724164646564000004e054686520676976656e206d656d626572207761732061646465643b2073656520746865207472616e73616374696f6e20666f722077686f2e344d656d62657252656d6f766564000104e854686520676976656e206d656d626572207761732072656d6f7665643b2073656520746865207472616e73616374696f6e20666f722077686f2e384d656d6265727353776170706564000204d854776f206d656d62657273207765726520737761707065643b2073656520746865207472616e73616374696f6e20666f722077686f2e304d656d6265727352657365740003041501546865206d656d62657273686970207761732072657365743b2073656520746865207472616e73616374696f6e20666f722077686f20746865206e6577207365742069732e284b65794368616e676564000404844f6e65206f6620746865206d656d6265727327206b657973206368616e6765642e1444756d6d790005046c5068616e746f6d206d656d6265722c206e6576657220757365642e047c54686520604576656e746020656e756d206f6620746869732070616c6c6574c80c4470616c6c65745f6d656d626572736869701870616c6c6574144576656e740804540004490001182c4d656d6265724164646564000004e054686520676976656e206d656d626572207761732061646465643b2073656520746865207472616e73616374696f6e20666f722077686f2e344d656d62657252656d6f766564000104e854686520676976656e206d656d626572207761732072656d6f7665643b2073656520746865207472616e73616374696f6e20666f722077686f2e384d656d6265727353776170706564000204d854776f206d656d62657273207765726520737761707065643b2073656520746865207472616e73616374696f6e20666f722077686f2e304d656d6265727352657365740003041501546865206d656d62657273686970207761732072657365743b2073656520746865207472616e73616374696f6e20666f722077686f20746865206e6577207365742069732e284b65794368616e676564000404844f6e65206f6620746865206d656d6265727327206b657973206368616e6765642e1444756d6d790005046c5068616e746f6d206d656d6265722c206e6576657220757365642e047c54686520604576656e746020656e756d206f6620746869732070616c6c6574cc0c3870616c6c65745f7574696c6974791870616c6c6574144576656e74000118404261746368496e746572727570746564080114696e64657810010c7533320001146572726f7268013444697370617463684572726f7200000855014261746368206f66206469737061746368657320646964206e6f7420636f6d706c6574652066756c6c792e20496e646578206f66206669727374206661696c696e6720646973706174636820676976656e2c2061734877656c6c20617320746865206572726f722e384261746368436f6d706c65746564000104c84261746368206f66206469737061746368657320636f6d706c657465642066756c6c792077697468206e6f206572726f722e604261746368436f6d706c65746564576974684572726f7273000204b44261746368206f66206469737061746368657320636f6d706c657465642062757420686173206572726f72732e344974656d436f6d706c657465640003041d01412073696e676c65206974656d2077697468696e2061204261746368206f6620646973706174636865732068617320636f6d706c657465642077697468206e6f206572726f722e284974656d4661696c65640401146572726f7268013444697370617463684572726f720004041101412073696e676c65206974656d2077697468696e2061204261746368206f6620646973706174636865732068617320636f6d706c657465642077697468206572726f722e30446973706174636865644173040118726573756c74a001384469737061746368526573756c7400050458412063616c6c2077617320646973706174636865642e047c54686520604576656e746020656e756d206f6620746869732070616c6c6574d00c2c70616c6c65745f7375646f1870616c6c6574144576656e7404045400011014537564696404012c7375646f5f726573756c74a001384469737061746368526573756c7404b454686520726573756c74206f66207468652063616c6c206d61646520627920746865207375646f20757365722e00047041207375646f2063616c6c206a75737420746f6f6b20706c6163652e284b65794368616e67656408010c6f6c64a801504f7074696f6e3c543a3a4163636f756e7449643e04b4546865206f6c64207375646f206b657920286966206f6e65207761732070726576696f75736c7920736574292e010c6e6577000130543a3a4163636f756e7449640488546865206e6577207375646f206b657920286966206f6e652077617320736574292e010478546865207375646f206b657920686173206265656e20757064617465642e284b657952656d6f76656400020480546865206b657920776173207065726d616e656e746c792072656d6f7665642e285375646f4173446f6e6504012c7375646f5f726573756c74a001384469737061746368526573756c7404b454686520726573756c74206f66207468652063616c6c206d61646520627920746865207375646f20757365722e0304c841205b7375646f5f61735d2850616c6c65743a3a7375646f5f6173292063616c6c206a75737420746f6f6b20706c6163652e047c54686520604576656e746020656e756d206f6620746869732070616c6c6574d40c3c70616c6c65745f6d756c74697369671870616c6c6574144576656e740404540001102c4e65774d756c74697369670c0124617070726f76696e67000130543a3a4163636f756e7449640001206d756c7469736967000130543a3a4163636f756e74496400012463616c6c5f6861736804012043616c6c486173680000048c41206e6577206d756c7469736967206f7065726174696f6e2068617320626567756e2e404d756c7469736967417070726f76616c100124617070726f76696e67000130543a3a4163636f756e74496400012474696d65706f696e74d8017054696d65706f696e743c426c6f636b4e756d626572466f723c543e3e0001206d756c7469736967000130543a3a4163636f756e74496400012463616c6c5f6861736804012043616c6c48617368000104c841206d756c7469736967206f7065726174696f6e20686173206265656e20617070726f76656420627920736f6d656f6e652e404d756c74697369674578656375746564140124617070726f76696e67000130543a3a4163636f756e74496400012474696d65706f696e74d8017054696d65706f696e743c426c6f636b4e756d626572466f723c543e3e0001206d756c7469736967000130543a3a4163636f756e74496400012463616c6c5f6861736804012043616c6c48617368000118726573756c74a001384469737061746368526573756c740002049c41206d756c7469736967206f7065726174696f6e20686173206265656e2065786563757465642e444d756c746973696743616e63656c6c656410012863616e63656c6c696e67000130543a3a4163636f756e74496400012474696d65706f696e74d8017054696d65706f696e743c426c6f636b4e756d626572466f723c543e3e0001206d756c7469736967000130543a3a4163636f756e74496400012463616c6c5f6861736804012043616c6c48617368000304a041206d756c7469736967206f7065726174696f6e20686173206265656e2063616e63656c6c65642e047c54686520604576656e746020656e756d206f6620746869732070616c6c6574d8083c70616c6c65745f6d756c74697369672454696d65706f696e74042c426c6f636b4e756d62657201100008011868656967687410012c426c6f636b4e756d626572000114696e64657810010c7533320000dc0c3c70616c6c65745f707265696d6167651870616c6c6574144576656e7404045400010c144e6f7465640401106861736834011c543a3a48617368000004684120707265696d61676520686173206265656e206e6f7465642e245265717565737465640401106861736834011c543a3a48617368000104784120707265696d61676520686173206265656e207265717565737465642e1c436c65617265640401106861736834011c543a3a486173680002046c4120707265696d616765206861732062656e20636c65617265642e047c54686520604576656e746020656e756d206f6620746869732070616c6c6574e00c4070616c6c65745f7363686564756c65721870616c6c6574144576656e74040454000124245363686564756c65640801107768656e100144426c6f636b4e756d626572466f723c543e000114696e64657810010c753332000004505363686564756c656420736f6d65207461736b2e2043616e63656c65640801107768656e100144426c6f636b4e756d626572466f723c543e000114696e64657810010c7533320001044c43616e63656c656420736f6d65207461736b2e28446973706174636865640c01107461736be401785461736b416464726573733c426c6f636b4e756d626572466f723c543e3e0001086964e801404f7074696f6e3c5461736b4e616d653e000118726573756c74a001384469737061746368526573756c74000204544469737061746368656420736f6d65207461736b2e2052657472795365741001107461736be401785461736b416464726573733c426c6f636b4e756d626572466f723c543e3e0001086964e801404f7074696f6e3c5461736b4e616d653e000118706572696f64100144426c6f636b4e756d626572466f723c543e00011c726574726965730801087538000304a0536574206120726574727920636f6e66696775726174696f6e20666f7220736f6d65207461736b2e38526574727943616e63656c6c65640801107461736be401785461736b416464726573733c426c6f636b4e756d626572466f723c543e3e0001086964e801404f7074696f6e3c5461736b4e616d653e000404ac43616e63656c206120726574727920636f6e66696775726174696f6e20666f7220736f6d65207461736b2e3c43616c6c556e617661696c61626c650801107461736be401785461736b416464726573733c426c6f636b4e756d626572466f723c543e3e0001086964e801404f7074696f6e3c5461736b4e616d653e00050429015468652063616c6c20666f72207468652070726f7669646564206861736820776173206e6f7420666f756e6420736f20746865207461736b20686173206265656e2061626f727465642e38506572696f6469634661696c65640801107461736be401785461736b416464726573733c426c6f636b4e756d626572466f723c543e3e0001086964e801404f7074696f6e3c5461736b4e616d653e0006043d0154686520676976656e207461736b2077617320756e61626c6520746f2062652072656e657765642073696e636520746865206167656e64612069732066756c6c206174207468617420626c6f636b2e2c52657472794661696c65640801107461736be401785461736b416464726573733c426c6f636b4e756d626572466f723c543e3e0001086964e801404f7074696f6e3c5461736b4e616d653e0007085d0154686520676976656e207461736b2077617320756e61626c6520746f20626520726574726965642073696e636520746865206167656e64612069732066756c6c206174207468617420626c6f636b206f722074686572659c776173206e6f7420656e6f7567682077656967687420746f2072657363686564756c652069742e545065726d616e656e746c794f7665727765696768740801107461736be401785461736b416464726573733c426c6f636b4e756d626572466f723c543e3e0001086964e801404f7074696f6e3c5461736b4e616d653e000804f054686520676976656e207461736b2063616e206e657665722062652065786563757465642073696e6365206974206973206f7665727765696768742e04304576656e747320747970652ee400000408101000e804184f7074696f6e04045401040108104e6f6e6500000010536f6d650400040000010000ec0c3070616c6c65745f70726f78791870616c6c6574144576656e740404540001143450726f78794578656375746564040118726573756c74a001384469737061746368526573756c74000004bc412070726f78792077617320657865637574656420636f72726563746c792c20776974682074686520676976656e2e2c507572654372656174656410011070757265000130543a3a4163636f756e74496400010c77686f000130543a3a4163636f756e74496400012870726f78795f74797065f00130543a3a50726f787954797065000150646973616d626967756174696f6e5f696e6465789c010c753136000108dc412070757265206163636f756e7420686173206265656e2063726561746564206279206e65772070726f7879207769746820676976656e90646973616d626967756174696f6e20696e64657820616e642070726f787920747970652e24416e6e6f756e6365640c01107265616c000130543a3a4163636f756e74496400011470726f7879000130543a3a4163636f756e74496400012463616c6c5f6861736834013443616c6c486173684f663c543e000204e0416e20616e6e6f756e63656d656e742077617320706c6163656420746f206d616b6520612063616c6c20696e20746865206675747572652e2850726f7879416464656410012464656c656761746f72000130543a3a4163636f756e74496400012464656c656761746565000130543a3a4163636f756e74496400012870726f78795f74797065f00130543a3a50726f78795479706500011464656c6179100144426c6f636b4e756d626572466f723c543e00030448412070726f7879207761732061646465642e3050726f787952656d6f76656410012464656c656761746f72000130543a3a4163636f756e74496400012464656c656761746565000130543a3a4163636f756e74496400012870726f78795f74797065f00130543a3a50726f78795479706500011464656c6179100144426c6f636b4e756d626572466f723c543e00040450412070726f7879207761732072656d6f7665642e047c54686520604576656e746020656e756d206f6620746869732070616c6c6574f008586e6f64655f73756274656e736f725f72756e74696d652450726f78795479706500013c0c416e79000000144f776e65720001002c4e6f6e437269746963616c0002002c4e6f6e5472616e736665720003001853656e617465000400304e6f6e46756e676962696c650005002c547269756d76697261746500060028476f7665726e616e63650007001c5374616b696e6700080030526567697374726174696f6e000900205472616e73666572000a0034536d616c6c5472616e73666572000b002c526f6f7457656967687473000c00244368696c644b657973000d00505375646f556e636865636b6564536574436f6465000e0000f40c3c70616c6c65745f72656769737472791870616c6c6574144576656e740404540001082c4964656e7469747953657404010c77686f000130543a3a4163636f756e74496404a0546865206163636f756e742074686174207265676973746572656420746865206964656e746974790004a4456d6974746564207768656e206120757365722072656769737465727320616e206964656e74697479444964656e74697479446973736f6c76656404010c77686f000130543a3a4163636f756e744964049c546865206163636f756e74207468617420646973736f6c76656420746865206964656e746974790104a4456d6974746564207768656e2061207573657220646973736f6c76657320616e206964656e74697479047c54686520604576656e746020656e756d206f6620746869732070616c6c6574f80c4870616c6c65745f636f6d6d69746d656e74731870616c6c6574144576656e7404045400010428436f6d6d69746d656e740801186e65747569649c010c7531360470546865206e6574756964206f662074686520636f6d6d69746d656e74010c77686f000130543a3a4163636f756e744964042c546865206163636f756e740004504120636f6d6d69746d656e742077617320736574047c54686520604576656e746020656e756d206f6620746869732070616c6c6574fc0c4870616c6c65745f61646d696e5f7574696c731870616c6c6574144576656e74040454000100047c54686520604576656e746020656e756d206f6620746869732070616c6c657401010c4070616c6c65745f736166655f6d6f64651870616c6c6574144576656e740404540001201c456e7465726564040114756e74696c100144426c6f636b4e756d626572466f723c543e000004dc54686520736166652d6d6f64652077617320656e746572656420756e74696c20696e636c75736976656c79207468697320626c6f636b2e20457874656e646564040114756e74696c100144426c6f636b4e756d626572466f723c543e000104e054686520736166652d6d6f64652077617320657874656e64656420756e74696c20696e636c75736976656c79207468697320626c6f636b2e18457869746564040118726561736f6e0501012845786974526561736f6e000204ac4578697465642074686520736166652d6d6f646520666f72206120737065636966696320726561736f6e2e344465706f736974506c6163656408011c6163636f756e74000130543a3a4163636f756e744964000118616d6f756e7418013042616c616e63654f663c543e0003042501416e206163636f756e742072657365727665642066756e647320666f722065697468657220656e746572696e67206f7220657874656e64696e672074686520736166652d6d6f64652e3c4465706f73697452656c656173656408011c6163636f756e74000130543a3a4163636f756e744964000118616d6f756e7418013042616c616e63654f663c543e000404d0416e206163636f756e7420686164206120726573657276652072656c65617365642074686174207761732072657365727665642e384465706f736974536c617368656408011c6163636f756e74000130543a3a4163636f756e744964000118616d6f756e7418013042616c616e63654f663c543e000504c4416e206163636f756e7420686164207265736572766520736c61736865642074686174207761732072657365727665642e3443616e6e6f744465706f73697400060cf4436f756c64206e6f7420686f6c642066756e647320666f7220656e746572696e67206f7220657874656e64696e672074686520736166652d6d6f64652e00c054686973206572726f7220636f6d65732066726f6d2074686520756e6465726c79696e67206043757272656e6379602e3443616e6e6f7452656c6561736500070c0101436f756c64206e6f742072656c656173652066756e647320666f7220656e746572696e67206f7220657874656e64696e672074686520736166652d6d6f64652e00c054686973206572726f7220636f6d65732066726f6d2074686520756e6465726c79696e67206043757272656e6379602e047c54686520604576656e746020656e756d206f6620746869732070616c6c657405010c4070616c6c65745f736166655f6d6f64651870616c6c65742845786974526561736f6e0001081c54696d656f757400000014466f7263650001000009010c3c70616c6c65745f657468657265756d1870616c6c6574144576656e7400010420457865637574656414011066726f6d0d01011048313630000108746f0d010110483136300001407472616e73616374696f6e5f686173683401104832353600012c657869745f726561736f6e1501012845786974526561736f6e00012865787472615f6461746138011c5665633c75383e000004c8416e20657468657265756d207472616e73616374696f6e20776173207375636365737366756c6c792065786563757465642e047c54686520604576656e746020656e756d206f6620746869732070616c6c65740d01083c7072696d69746976655f7479706573104831363000000400110101205b75383b2032305d0000110100000314000000080015010c2065766d5f636f7265146572726f722845786974526561736f6e0001101c5375636365656404001901012c4578697453756363656564000000144572726f7204001d010124457869744572726f720001001852657665727404002d0101284578697452657665727400020014466174616c04003101012445786974466174616c0003000019010c2065766d5f636f7265146572726f722c457869745375636365656400010c1c53746f707065640000002052657475726e6564000100205375696369646564000200001d010c2065766d5f636f7265146572726f7224457869744572726f7200014038537461636b556e646572666c6f7700000034537461636b4f766572666c6f770001002c496e76616c69644a756d7000020030496e76616c696452616e67650003004444657369676e61746564496e76616c69640004002c43616c6c546f6f446565700005003c437265617465436f6c6c6973696f6e0006004c437265617465436f6e74726163744c696d69740007002c496e76616c6964436f64650400210101184f70636f6465000f002c4f75744f664f6666736574000800204f75744f66476173000900244f75744f6646756e64000a002c5043556e646572666c6f77000b002c437265617465456d707479000c00144f74686572040025010144436f773c277374617469632c207374723e000d00204d61784e6f6e6365000e000021010c2065766d5f636f7265186f70636f6465184f70636f646500000400080108753800002501040c436f770404540129010004002901000000290100000502002d010c2065766d5f636f7265146572726f7228457869745265766572740001042052657665727465640000000031010c2065766d5f636f7265146572726f722445786974466174616c000110304e6f74537570706f7274656400000048556e68616e646c6564496e746572727570740001004043616c6c4572726f724173466174616c04001d010124457869744572726f72000200144f74686572040025010144436f773c277374617469632c207374723e0003000035010c2870616c6c65745f65766d1870616c6c6574144576656e740404540001140c4c6f6704010c6c6f673901010c4c6f670000047c457468657265756d206576656e74732066726f6d20636f6e7472616374732e1c4372656174656404011c616464726573730d01011048313630000104b44120636f6e747261637420686173206265656e206372656174656420617420676976656e20616464726573732e34437265617465644661696c656404011c616464726573730d0101104831363000020405014120636f6e74726163742077617320617474656d7074656420746f20626520637265617465642c206275742074686520657865637574696f6e206661696c65642e20457865637574656404011c616464726573730d01011048313630000304f84120636f6e747261637420686173206265656e206578656375746564207375636365737366756c6c79207769746820737461746573206170706c6965642e3845786563757465644661696c656404011c616464726573730d0101104831363000040465014120636f6e747261637420686173206265656e2065786563757465642077697468206572726f72732e20537461746573206172652072657665727465642077697468206f6e6c79206761732066656573206170706c6965642e047c54686520604576656e746020656e756d206f6620746869732070616c6c657439010c20657468657265756d0c6c6f670c4c6f6700000c011c616464726573730d01011048313630000118746f70696373b401245665633c483235363e00011064617461380114427974657300003d010c3c70616c6c65745f626173655f6665651870616c6c6574144576656e7400010c404e65774261736546656550657247617304010c66656541010110553235360000003c426173654665654f766572666c6f77000100344e6577456c6173746963697479040128656c61737469636974794901011c5065726d696c6c000200047c54686520604576656e746020656e756d206f6620746869732070616c6c65744101083c7072696d69746976655f7479706573105532353600000400450101205b7536343b20345d0000450100000304000000180049010c3473705f61726974686d65746963287065725f7468696e67731c5065726d696c6c0000040010010c75333200004d010c3070616c6c65745f6472616e641870616c6c6574144576656e740404540001084c426561636f6e436f6e6669674368616e676564000000204e657750756c7365040118726f756e6473510101405665633c526f756e644e756d6265723e000104805375636365737366756c6c79207365742061206e65772070756c73652873292e047c54686520604576656e746020656e756d206f6620746869732070616c6c657451010000021800550108306672616d655f73797374656d14506861736500010c384170706c7945787472696e736963040010010c7533320000003046696e616c697a6174696f6e00010038496e697469616c697a6174696f6e000200005901000002e4005d0108306672616d655f73797374656d584c61737452756e74696d6555706772616465496e666f0000080130737065635f76657273696f6e6101014c636f6465633a3a436f6d706163743c7533323e000124737065635f6e616d652901016473705f72756e74696d653a3a52756e74696d65537472696e67000061010000061000650108306672616d655f73797374656d60436f646555706772616465417574686f72697a6174696f6e0404540000080124636f64655f6861736834011c543a3a48617368000134636865636b5f76657273696f6e240110626f6f6c000069010c306672616d655f73797374656d1870616c6c65741043616c6c04045400012c1872656d61726b04011872656d61726b38011c5665633c75383e00000c684d616b6520736f6d65206f6e2d636861696e2072656d61726b2e008843616e20626520657865637574656420627920657665727920606f726967696e602e387365745f686561705f7061676573040114706167657318010c753634000104f853657420746865206e756d626572206f6620706167657320696e2074686520576562417373656d626c7920656e7669726f6e6d656e74277320686561702e207365745f636f6465040110636f646538011c5665633c75383e0002046453657420746865206e65772072756e74696d6520636f64652e5c7365745f636f64655f776974686f75745f636865636b73040110636f646538011c5665633c75383e000310190153657420746865206e65772072756e74696d6520636f646520776974686f757420646f696e6720616e7920636865636b73206f662074686520676976656e2060636f6465602e0051014e6f746520746861742072756e74696d652075706772616465732077696c6c206e6f742072756e20696620746869732069732063616c6c656420776974682061206e6f742d696e6372656173696e6720737065632076657273696f6e212c7365745f73746f726167650401146974656d736d0101345665633c4b657956616c75653e0004046853657420736f6d65206974656d73206f662073746f726167652e306b696c6c5f73746f726167650401106b657973750101205665633c4b65793e000504744b696c6c20736f6d65206974656d732066726f6d2073746f726167652e2c6b696c6c5f70726566697808011870726566697838010c4b657900011c7375626b65797310010c75333200061011014b696c6c20616c6c2073746f72616765206974656d7320776974682061206b657920746861742073746172747320776974682074686520676976656e207072656669782e0039012a2a4e4f54453a2a2a2057652072656c79206f6e2074686520526f6f74206f726967696e20746f2070726f7669646520757320746865206e756d626572206f66207375626b65797320756e6465723d0174686520707265666978207765206172652072656d6f76696e6720746f2061636375726174656c792063616c63756c6174652074686520776569676874206f6620746869732066756e6374696f6e2e4472656d61726b5f776974685f6576656e7404011872656d61726b38011c5665633c75383e000704a44d616b6520736f6d65206f6e2d636861696e2072656d61726b20616e6420656d6974206576656e742e44617574686f72697a655f75706772616465040124636f64655f6861736834011c543a3a486173680009106101417574686f72697a6520616e207570677261646520746f206120676976656e2060636f64655f686173686020666f72207468652072756e74696d652e205468652072756e74696d652063616e20626520737570706c696564186c617465722e007c546869732063616c6c20726571756972657320526f6f74206f726967696e2e80617574686f72697a655f757067726164655f776974686f75745f636865636b73040124636f64655f6861736834011c543a3a48617368000a206101417574686f72697a6520616e207570677261646520746f206120676976656e2060636f64655f686173686020666f72207468652072756e74696d652e205468652072756e74696d652063616e20626520737570706c696564186c617465722e005d015741524e494e473a205468697320617574686f72697a657320616e207570677261646520746861742077696c6c2074616b6520706c61636520776974686f757420616e792073616665747920636865636b732c20666f7259016578616d706c652074686174207468652073706563206e616d652072656d61696e73207468652073616d6520616e642074686174207468652076657273696f6e206e756d62657220696e637265617365732e204e6f74f07265636f6d6d656e64656420666f72206e6f726d616c207573652e205573652060617574686f72697a655f757067726164656020696e73746561642e007c546869732063616c6c20726571756972657320526f6f74206f726967696e2e606170706c795f617574686f72697a65645f75706772616465040110636f646538011c5665633c75383e000b24550150726f766964652074686520707265696d616765202872756e74696d652062696e617279292060636f64656020666f7220616e2075706772616465207468617420686173206265656e20617574686f72697a65642e00490149662074686520617574686f72697a6174696f6e20726571756972656420612076657273696f6e20636865636b2c20746869732063616c6c2077696c6c20656e73757265207468652073706563206e616d65e872656d61696e7320756e6368616e67656420616e6420746861742074686520737065632076657273696f6e2068617320696e637265617365642e005901446570656e64696e67206f6e207468652072756e74696d65277320604f6e536574436f64656020636f6e66696775726174696f6e2c20746869732066756e6374696f6e206d6179206469726563746c79206170706c791101746865206e65772060636f64656020696e207468652073616d6520626c6f636b206f7220617474656d707420746f207363686564756c652074686520757067726164652e0060416c6c206f726967696e732061726520616c6c6f7765642e040d01436f6e7461696e7320612076617269616e742070657220646973706174636861626c652065787472696e736963207468617420746869732070616c6c6574206861732e6d010000027101007101000004083838007501000002380079010c306672616d655f73797374656d186c696d69747330426c6f636b5765696768747300000c0128626173655f626c6f636b2c01185765696768740001246d61785f626c6f636b2c01185765696768740001247065725f636c6173737d0101845065724469737061746368436c6173733c57656967687473506572436c6173733e00007d010c346672616d655f737570706f7274206469737061746368405065724469737061746368436c617373040454018101000c01186e6f726d616c810101045400012c6f7065726174696f6e616c81010104540001246d616e6461746f72798101010454000081010c306672616d655f73797374656d186c696d6974733c57656967687473506572436c6173730000100138626173655f65787472696e7369632c01185765696768740001346d61785f65787472696e736963850101384f7074696f6e3c5765696768743e0001246d61785f746f74616c850101384f7074696f6e3c5765696768743e0001207265736572766564850101384f7074696f6e3c5765696768743e0000850104184f7074696f6e040454012c0108104e6f6e6500000010536f6d6504002c000001000089010c306672616d655f73797374656d186c696d6974732c426c6f636b4c656e677468000004010c6d61788d0101545065724469737061746368436c6173733c7533323e00008d010c346672616d655f737570706f7274206469737061746368405065724469737061746368436c6173730404540110000c01186e6f726d616c1001045400012c6f7065726174696f6e616c100104540001246d616e6461746f72791001045400009101082873705f776569676874733c52756e74696d65446257656967687400000801107265616418010c753634000114777269746518010c75363400009501082873705f76657273696f6e3852756e74696d6556657273696f6e0000200124737065635f6e616d652901013452756e74696d65537472696e67000124696d706c5f6e616d652901013452756e74696d65537472696e67000144617574686f72696e675f76657273696f6e10010c753332000130737065635f76657273696f6e10010c753332000130696d706c5f76657273696f6e10010c753332000110617069739901011c4170697356656300014c7472616e73616374696f6e5f76657273696f6e10010c75333200013473746174655f76657273696f6e080108753800009901040c436f77040454019d010004009d010000009d01000002a10100a10100000408a5011000a501000003080000000800a9010c306672616d655f73797374656d1870616c6c6574144572726f720404540001243c496e76616c6964537065634e616d650000081101546865206e616d65206f662073706563696669636174696f6e20646f6573206e6f74206d61746368206265747765656e207468652063757272656e742072756e74696d6550616e6420746865206e65772072756e74696d652e685370656356657273696f6e4e65656473546f496e63726561736500010841015468652073706563696669636174696f6e2076657273696f6e206973206e6f7420616c6c6f77656420746f206465637265617365206265747765656e207468652063757272656e742072756e74696d6550616e6420746865206e65772072756e74696d652e744661696c6564546f4578747261637452756e74696d6556657273696f6e00020cec4661696c656420746f2065787472616374207468652072756e74696d652076657273696f6e2066726f6d20746865206e65772072756e74696d652e0009014569746865722063616c6c696e672060436f72655f76657273696f6e60206f72206465636f64696e67206052756e74696d6556657273696f6e60206661696c65642e4c4e6f6e44656661756c74436f6d706f73697465000304fc537569636964652063616c6c6564207768656e20746865206163636f756e7420686173206e6f6e2d64656661756c7420636f6d706f7369746520646174612e3c4e6f6e5a65726f526566436f756e74000404350154686572652069732061206e6f6e2d7a65726f207265666572656e636520636f756e742070726576656e74696e6720746865206163636f756e742066726f6d206265696e67207075726765642e3043616c6c46696c7465726564000504d0546865206f726967696e2066696c7465722070726576656e74207468652063616c6c20746f20626520646973706174636865642e6c4d756c7469426c6f636b4d6967726174696f6e734f6e676f696e67000604550141206d756c74692d626c6f636b206d6967726174696f6e206973206f6e676f696e6720616e642070726576656e7473207468652063757272656e7420636f64652066726f6d206265696e67207265706c616365642e444e6f7468696e67417574686f72697a6564000704584e6f207570677261646520617574686f72697a65642e30556e617574686f72697a656400080494546865207375626d697474656420636f6465206973206e6f7420617574686f72697a65642e046c4572726f7220666f72207468652053797374656d2070616c6c6574ad010c4c626f756e6465645f636f6c6c656374696f6e732c626f756e6465645f76656328426f756e6465645665630804540134045300000400b401185665633c543e0000b1010c4070616c6c65745f74696d657374616d701870616c6c65741043616c6c0404540001040c73657404010c6e6f77300124543a3a4d6f6d656e7400004c54536574207468652063757272656e742074696d652e005501546869732063616c6c2073686f756c6420626520696e766f6b65642065786163746c79206f6e63652070657220626c6f636b2e2049742077696c6c2070616e6963206174207468652066696e616c697a6174696f6ed470686173652c20696620746869732063616c6c206861736e2774206265656e20696e766f6b656420627920746861742074696d652e0041015468652074696d657374616d702073686f756c642062652067726561746572207468616e207468652070726576696f7573206f6e652062792074686520616d6f756e7420737065636966696564206279685b60436f6e6669673a3a4d696e696d756d506572696f64605d2e00c4546865206469737061746368206f726967696e20666f7220746869732063616c6c206d757374206265205f4e6f6e655f2e0051015468697320646973706174636820636c617373206973205f4d616e6461746f72795f20746f20656e73757265206974206765747320657865637574656420696e2074686520626c6f636b2e204265206177617265510174686174206368616e67696e672074686520636f6d706c6578697479206f6620746869732063616c6c20636f756c6420726573756c742065786861757374696e6720746865207265736f757263657320696e206184626c6f636b20746f206578656375746520616e79206f746865722063616c6c732e0034232320436f6d706c657869747931012d20604f2831296020284e6f7465207468617420696d706c656d656e746174696f6e73206f6620604f6e54696d657374616d7053657460206d75737420616c736f20626520604f283129602955012d20312073746f72616765207265616420616e6420312073746f72616765206d75746174696f6e2028636f64656320604f283129602062656361757365206f6620604469645570646174653a3a74616b656020696e402020606f6e5f66696e616c697a656029d42d2031206576656e742068616e646c657220606f6e5f74696d657374616d705f736574602e204d75737420626520604f283129602e040d01436f6e7461696e7320612076617269616e742070657220646973706174636861626c652065787472696e736963207468617420746869732070616c6c6574206861732eb5010c4c626f756e6465645f636f6c6c656374696f6e732c626f756e6465645f76656328426f756e64656456656308045401b901045300000400bd0101185665633c543e0000b901104473705f636f6e73656e7375735f617572611c737232353531392c6170705f73723235353139185075626c69630000040004013c737232353531393a3a5075626c69630000bd01000002b90100c101084873705f636f6e73656e7375735f736c6f747310536c6f740000040018010c7536340000c501083870616c6c65745f6772616e6470612c53746f726564537461746504044e01100110104c6976650000003050656e64696e6750617573650801307363686564756c65645f61741001044e00011464656c61791001044e000100185061757365640002003450656e64696e67526573756d650801307363686564756c65645f61741001044e00011464656c61791001044e00030000c901083870616c6c65745f6772616e6470614c53746f72656450656e64696e674368616e676508044e0110144c696d697400001001307363686564756c65645f61741001044e00011464656c61791001044e0001406e6578745f617574686f726974696573cd01016c426f756e646564417574686f726974794c6973743c4c696d69743e000118666f72636564d10101244f7074696f6e3c4e3e0000cd010c4c626f756e6465645f636f6c6c656374696f6e73407765616b5f626f756e6465645f766563385765616b426f756e64656456656308045401840453000004008001185665633c543e0000d10104184f7074696f6e04045401100108104e6f6e6500000010536f6d650400100000010000d5010c3870616c6c65745f6772616e6470611870616c6c65741043616c6c04045400010c4c7265706f72745f65717569766f636174696f6e08014865717569766f636174696f6e5f70726f6f66d90101c8426f783c45717569766f636174696f6e50726f6f663c543a3a486173682c20426c6f636b4e756d626572466f723c543e3e3e00013c6b65795f6f776e65725f70726f6f6601020140543a3a4b65794f776e657250726f6f6600001009015265706f727420766f7465722065717569766f636174696f6e2f6d69736265686176696f722e2054686973206d6574686f642077696c6c2076657269667920746865f465717569766f636174696f6e2070726f6f6620616e642076616c69646174652074686520676976656e206b6579206f776e6572736869702070726f6f66f8616761696e73742074686520657874726163746564206f6666656e6465722e20496620626f7468206172652076616c69642c20746865206f6666656e63654477696c6c206265207265706f727465642e707265706f72745f65717569766f636174696f6e5f756e7369676e656408014865717569766f636174696f6e5f70726f6f66d90101c8426f783c45717569766f636174696f6e50726f6f663c543a3a486173682c20426c6f636b4e756d626572466f723c543e3e3e00013c6b65795f6f776e65725f70726f6f6601020140543a3a4b65794f776e657250726f6f6600012409015265706f727420766f7465722065717569766f636174696f6e2f6d69736265686176696f722e2054686973206d6574686f642077696c6c2076657269667920746865f465717569766f636174696f6e2070726f6f6620616e642076616c69646174652074686520676976656e206b6579206f776e6572736869702070726f6f66f8616761696e73742074686520657874726163746564206f6666656e6465722e20496620626f7468206172652076616c69642c20746865206f6666656e63654477696c6c206265207265706f727465642e000d01546869732065787472696e736963206d7573742062652063616c6c656420756e7369676e656420616e642069742069732065787065637465642074686174206f6e6c791501626c6f636b20617574686f72732077696c6c2063616c6c206974202876616c69646174656420696e206056616c6964617465556e7369676e656460292c2061732073756368150169662074686520626c6f636b20617574686f7220697320646566696e65642069742077696c6c20626520646566696e6564206173207468652065717569766f636174696f6e247265706f727465722e306e6f74655f7374616c6c656408011464656c6179100144426c6f636b4e756d626572466f723c543e00016c626573745f66696e616c697a65645f626c6f636b5f6e756d626572100144426c6f636b4e756d626572466f723c543e0002303d014e6f74652074686174207468652063757272656e7420617574686f7269747920736574206f6620746865204752414e4450412066696e616c6974792067616467657420686173207374616c6c65642e006101546869732077696c6c2074726967676572206120666f7263656420617574686f7269747920736574206368616e67652061742074686520626567696e6e696e67206f6620746865206e6578742073657373696f6e2c20746f6101626520656e6163746564206064656c61796020626c6f636b7320616674657220746861742e20546865206064656c6179602073686f756c64206265206869676820656e6f75676820746f20736166656c7920617373756d654901746861742074686520626c6f636b207369676e616c6c696e672074686520666f72636564206368616e67652077696c6c206e6f742062652072652d6f7267656420652e672e203130303020626c6f636b732e5d0154686520626c6f636b2070726f64756374696f6e207261746520287768696368206d617920626520736c6f77656420646f776e2062656361757365206f662066696e616c697479206c616767696e67292073686f756c64510162652074616b656e20696e746f206163636f756e74207768656e2063686f6f73696e6720746865206064656c6179602e20546865204752414e44504120766f74657273206261736564206f6e20746865206e65775501617574686f726974792077696c6c20737461727420766f74696e67206f6e20746f70206f662060626573745f66696e616c697a65645f626c6f636b5f6e756d6265726020666f72206e65772066696e616c697a65644d01626c6f636b732e2060626573745f66696e616c697a65645f626c6f636b5f6e756d626572602073686f756c64206265207468652068696768657374206f6620746865206c61746573742066696e616c697a6564c4626c6f636b206f6620616c6c2076616c696461746f7273206f6620746865206e657720617574686f72697479207365742e00584f6e6c792063616c6c61626c6520627920726f6f742e040d01436f6e7461696e7320612076617269616e742070657220646973706174636861626c652065787472696e736963207468617420746869732070616c6c6574206861732ed901085073705f636f6e73656e7375735f6772616e6470614445717569766f636174696f6e50726f6f660804480134044e0110000801187365745f6964180114536574496400013065717569766f636174696f6edd01014845717569766f636174696f6e3c482c204e3e0000dd01085073705f636f6e73656e7375735f6772616e6470613045717569766f636174696f6e0804480134044e011001081c507265766f74650400e10101890166696e616c6974795f6772616e6470613a3a45717569766f636174696f6e3c417574686f7269747949642c2066696e616c6974795f6772616e6470613a3a507265766f74653c0a482c204e3e2c20417574686f726974795369676e61747572652c3e00000024507265636f6d6d69740400f50101910166696e616c6974795f6772616e6470613a3a45717569766f636174696f6e3c417574686f7269747949642c2066696e616c6974795f6772616e6470613a3a507265636f6d6d69740a3c482c204e3e2c20417574686f726974795369676e61747572652c3e00010000e101084066696e616c6974795f6772616e6470613045717569766f636174696f6e0c0849640188045601e501045301e90100100130726f756e645f6e756d62657218010c7536340001206964656e7469747988010849640001146669727374f101011828562c2053290001187365636f6e64f101011828562c2053290000e501084066696e616c6974795f6772616e6470611c507265766f74650804480134044e01100008012c7461726765745f68617368340104480001347461726765745f6e756d6265721001044e0000e9010c5073705f636f6e73656e7375735f6772616e6470610c617070245369676e617475726500000400ed010148656432353531393a3a5369676e61747572650000ed01000003400000000800f10100000408e501e90100f501084066696e616c6974795f6772616e6470613045717569766f636174696f6e0c0849640188045601f901045301e90100100130726f756e645f6e756d62657218010c7536340001206964656e7469747988010849640001146669727374fd01011828562c2053290001187365636f6e64fd01011828562c2053290000f901084066696e616c6974795f6772616e64706124507265636f6d6d69740804480134044e01100008012c7461726765745f68617368340104480001347461726765745f6e756d6265721001044e0000fd0100000408f901e901000102081c73705f636f726510566f69640001000005020c3870616c6c65745f6772616e6470611870616c6c6574144572726f7204045400011c2c50617573654661696c65640000080501417474656d707420746f207369676e616c204752414e445041207061757365207768656e2074686520617574686f72697479207365742069736e2774206c697665a42865697468657220706175736564206f7220616c72656164792070656e64696e67207061757365292e30526573756d654661696c65640001081101417474656d707420746f207369676e616c204752414e44504120726573756d65207768656e2074686520617574686f72697479207365742069736e277420706175736564a028656974686572206c697665206f7220616c72656164792070656e64696e6720726573756d65292e344368616e676550656e64696e67000204e8417474656d707420746f207369676e616c204752414e445041206368616e67652077697468206f6e6520616c72656164792070656e64696e672e1c546f6f536f6f6e000304bc43616e6e6f74207369676e616c20666f72636564206368616e676520736f20736f6f6e206166746572206c6173742e60496e76616c69644b65794f776e65727368697050726f6f66000404310141206b6579206f776e6572736869702070726f6f662070726f76696465642061732070617274206f6620616e2065717569766f636174696f6e207265706f727420697320696e76616c69642e60496e76616c696445717569766f636174696f6e50726f6f660005043101416e2065717569766f636174696f6e2070726f6f662070726f76696465642061732070617274206f6620616e2065717569766f636174696f6e207265706f727420697320696e76616c69642e584475706c69636174654f6666656e63655265706f727400060415014120676976656e2065717569766f636174696f6e207265706f72742069732076616c69642062757420616c72656164792070726576696f75736c79207265706f727465642e048054686520604572726f726020656e756d206f6620746869732070616c6c65742e09020c4c626f756e6465645f636f6c6c656374696f6e73407765616b5f626f756e6465645f766563385765616b426f756e646564566563080454010d02045300000400150201185665633c543e00000d020c3c70616c6c65745f62616c616e6365731474797065732c42616c616e63654c6f636b041c42616c616e63650118000c01086964a50101384c6f636b4964656e746966696572000118616d6f756e7418011c42616c616e636500011c726561736f6e731102011c526561736f6e73000011020c3c70616c6c65745f62616c616e6365731474797065731c526561736f6e7300010c0c466565000000104d6973630001000c416c6c0002000015020000020d020019020c4c626f756e6465645f636f6c6c656374696f6e732c626f756e6465645f76656328426f756e646564566563080454011d02045300000400210201185665633c543e00001d020c3c70616c6c65745f62616c616e6365731474797065732c52657365727665446174610844526573657276654964656e74696669657201a5011c42616c616e63650118000801086964a5010144526573657276654964656e746966696572000118616d6f756e7418011c42616c616e6365000021020000021d020025020c4c626f756e6465645f636f6c6c656374696f6e732c626f756e6465645f76656328426f756e6465645665630804540129020453000004003d0201185665633c543e0000290214346672616d655f737570706f72741874726169747318746f6b656e73106d697363204964416d6f756e7408084964012d021c42616c616e636501180008010869642d0201084964000118616d6f756e7418011c42616c616e636500002d0208586e6f64655f73756274656e736f725f72756e74696d654452756e74696d65486f6c64526561736f6e00010c20507265696d61676504003102016c70616c6c65745f707265696d6167653a3a486f6c64526561736f6e000e0020526567697374727904003502016c70616c6c65745f72656769737472793a3a486f6c64526561736f6e00110020536166654d6f646504003902017070616c6c65745f736166655f6d6f64653a3a486f6c64526561736f6e0014000031020c3c70616c6c65745f707265696d6167651870616c6c657428486f6c64526561736f6e00010420507265696d6167650000000035020c3c70616c6c65745f72656769737472791870616c6c657428486f6c64526561736f6e0001044052656769737472794964656e746974790000000039020c4070616c6c65745f736166655f6d6f64651870616c6c657428486f6c64526561736f6e00010434456e7465724f72457874656e64000000003d0200000229020041020c4c626f756e6465645f636f6c6c656374696f6e732c626f756e6465645f76656328426f756e6465645665630804540145020453000004004d0201185665633c543e0000450214346672616d655f737570706f72741874726169747318746f6b656e73106d697363204964416d6f756e74080849640149021c42616c616e63650118000801086964490201084964000118616d6f756e7418011c42616c616e63650000490208586e6f64655f73756274656e736f725f72756e74696d654c52756e74696d65467265657a65526561736f6e000100004d0200000245020051020c3c70616c6c65745f62616c616e6365731870616c6c65741043616c6c080454000449000124507472616e736665725f616c6c6f775f646561746808011064657374550201504163636f756e7449644c6f6f6b75704f663c543e00011476616c7565300128543a3a42616c616e636500001cd45472616e7366657220736f6d65206c697175696420667265652062616c616e636520746f20616e6f74686572206163636f756e742e003501607472616e736665725f616c6c6f775f6465617468602077696c6c207365742074686520604672656542616c616e636560206f66207468652073656e64657220616e642072656365697665722e11014966207468652073656e6465722773206163636f756e742069732062656c6f7720746865206578697374656e7469616c206465706f736974206173206120726573756c74b06f6620746865207472616e736665722c20746865206163636f756e742077696c6c206265207265617065642e001501546865206469737061746368206f726967696e20666f7220746869732063616c6c206d75737420626520605369676e65646020627920746865207472616e736163746f722e38666f7263655f7472616e736665720c0118736f75726365550201504163636f756e7449644c6f6f6b75704f663c543e00011064657374550201504163636f756e7449644c6f6f6b75704f663c543e00011476616c7565300128543a3a42616c616e6365000208610145786163746c7920617320607472616e736665725f616c6c6f775f6465617468602c2065786365707420746865206f726967696e206d75737420626520726f6f7420616e642074686520736f75726365206163636f756e74446d6179206265207370656369666965642e4c7472616e736665725f6b6565705f616c69766508011064657374550201504163636f756e7449644c6f6f6b75704f663c543e00011476616c7565300128543a3a42616c616e6365000318590153616d6520617320746865205b607472616e736665725f616c6c6f775f6465617468605d2063616c6c2c206275742077697468206120636865636b207468617420746865207472616e736665722077696c6c206e6f74606b696c6c20746865206f726967696e206163636f756e742e00e8393925206f66207468652074696d6520796f752077616e74205b607472616e736665725f616c6c6f775f6465617468605d20696e73746561642e00f05b607472616e736665725f616c6c6f775f6465617468605d3a207374727563742e50616c6c65742e68746d6c236d6574686f642e7472616e73666572307472616e736665725f616c6c08011064657374550201504163636f756e7449644c6f6f6b75704f663c543e0001286b6565705f616c697665240110626f6f6c00043c05015472616e736665722074686520656e74697265207472616e7366657261626c652062616c616e63652066726f6d207468652063616c6c6572206163636f756e742e0059014e4f54453a20546869732066756e6374696f6e206f6e6c7920617474656d70747320746f207472616e73666572205f7472616e7366657261626c655f2062616c616e6365732e2054686973206d65616e7320746861746101616e79206c6f636b65642c2072657365727665642c206f72206578697374656e7469616c206465706f7369747320287768656e20606b6565705f616c6976656020697320607472756560292c2077696c6c206e6f742062655d017472616e7366657272656420627920746869732066756e6374696f6e2e20546f20656e73757265207468617420746869732066756e6374696f6e20726573756c747320696e2061206b696c6c6564206163636f756e742c4501796f75206d69676874206e65656420746f207072657061726520746865206163636f756e742062792072656d6f76696e6720616e79207265666572656e636520636f756e746572732c2073746f72616765406465706f736974732c206574632e2e2e00c0546865206469737061746368206f726967696e206f6620746869732063616c6c206d757374206265205369676e65642e00a02d206064657374603a2054686520726563697069656e74206f6620746865207472616e736665722e59012d20606b6565705f616c697665603a204120626f6f6c65616e20746f2064657465726d696e652069662074686520607472616e736665725f616c6c60206f7065726174696f6e2073686f756c642073656e6420616c6c4d0120206f66207468652066756e647320746865206163636f756e74206861732c2063617573696e67207468652073656e646572206163636f756e7420746f206265206b696c6c6564202866616c7365292c206f72590120207472616e736665722065766572797468696e6720657863657074206174206c6561737420746865206578697374656e7469616c206465706f7369742c2077686963682077696c6c2067756172616e74656520746f9c20206b656570207468652073656e646572206163636f756e7420616c697665202874727565292e3c666f7263655f756e7265736572766508010c77686f550201504163636f756e7449644c6f6f6b75704f663c543e000118616d6f756e74180128543a3a42616c616e636500050cb0556e7265736572766520736f6d652062616c616e63652066726f6d2061207573657220627920666f7263652e006c43616e206f6e6c792062652063616c6c656420627920524f4f542e40757067726164655f6163636f756e747304010c77686f5d0201445665633c543a3a4163636f756e7449643e0006207055706772616465206120737065636966696564206163636f756e742e00742d20606f726967696e603a204d75737420626520605369676e6564602e902d206077686f603a20546865206163636f756e7420746f2062652075706772616465642e005501546869732077696c6c20776169766520746865207472616e73616374696f6e20666565206966206174206c6561737420616c6c2062757420313025206f6620746865206163636f756e7473206e656564656420746f410162652075706772616465642e20285765206c657420736f6d65206e6f74206861766520746f206265207570677261646564206a75737420696e206f7264657220746f20616c6c6f7720666f722074686558706f73736962696c697479206f6620636875726e292e44666f7263655f7365745f62616c616e636508010c77686f550201504163636f756e7449644c6f6f6b75704f663c543e0001206e65775f66726565300128543a3a42616c616e636500080cac5365742074686520726567756c61722062616c616e6365206f66206120676976656e206163636f756e742e00b0546865206469737061746368206f726967696e20666f7220746869732063616c6c2069732060726f6f74602e6c666f7263655f61646a7573745f746f74616c5f69737375616e6365080124646972656374696f6e6102014c41646a7573746d656e74446972656374696f6e00011464656c7461300128543a3a42616c616e6365000914b841646a7573742074686520746f74616c2069737375616e636520696e20612073617475726174696e67207761792e00fc43616e206f6e6c792062652063616c6c656420627920726f6f7420616e6420616c77617973206e65656473206120706f736974697665206064656c7461602e002423204578616d706c65106275726e08011476616c7565300128543a3a42616c616e63650001286b6565705f616c697665240110626f6f6c000a1cfc4275726e2074686520737065636966696564206c697175696420667265652062616c616e63652066726f6d20746865206f726967696e206163636f756e742e002501496620746865206f726967696e2773206163636f756e7420656e64732075702062656c6f7720746865206578697374656e7469616c206465706f736974206173206120726573756c7409016f6620746865206275726e20616e6420606b6565705f616c697665602069732066616c73652c20746865206163636f756e742077696c6c206265207265617065642e005101556e6c696b652073656e64696e672066756e647320746f2061205f6275726e5f20616464726573732c207768696368206d6572656c79206d616b6573207468652066756e647320696e61636365737369626c652c21017468697320606275726e60206f7065726174696f6e2077696c6c2072656475636520746f74616c2069737375616e63652062792074686520616d6f756e74205f6275726e65645f2e040d01436f6e7461696e7320612076617269616e742070657220646973706174636861626c652065787472696e736963207468617420746869732070616c6c6574206861732e55020c2873705f72756e74696d65306d756c746961646472657373304d756c74694164647265737308244163636f756e7449640100304163636f756e74496e64657801a4011408496404000001244163636f756e74496400000014496e6465780400590201304163636f756e74496e6465780001000c526177040038011c5665633c75383e0002002441646472657373333204000401205b75383b2033325d000300244164647265737332300400110101205b75383b2032305d000400005902000006a4005d02000002000061020c3c70616c6c65745f62616c616e6365731474797065734c41646a7573746d656e74446972656374696f6e00010820496e6372656173650000002044656372656173650001000065020c3c70616c6c65745f62616c616e6365731870616c6c6574144572726f720804540004490001303856657374696e6742616c616e63650000049c56657374696e672062616c616e636520746f6f206869676820746f2073656e642076616c75652e544c69717569646974795265737472696374696f6e73000104c84163636f756e74206c6971756964697479207265737472696374696f6e732070726576656e74207769746864726177616c2e4c496e73756666696369656e7442616c616e63650002047842616c616e636520746f6f206c6f7720746f2073656e642076616c75652e484578697374656e7469616c4465706f736974000304ec56616c756520746f6f206c6f7720746f20637265617465206163636f756e742064756520746f206578697374656e7469616c206465706f7369742e34457870656e646162696c697479000404905472616e736665722f7061796d656e7420776f756c64206b696c6c206163636f756e742e5c4578697374696e6756657374696e675363686564756c65000504cc412076657374696e67207363686564756c6520616c72656164792065786973747320666f722074686973206163636f756e742e2c446561644163636f756e740006048c42656e6566696369617279206163636f756e74206d757374207072652d65786973742e3c546f6f4d616e795265736572766573000704b84e756d626572206f66206e616d65642072657365727665732065786365656420604d61785265736572766573602e30546f6f4d616e79486f6c6473000804f84e756d626572206f6620686f6c647320657863656564206056617269616e74436f756e744f663c543a3a52756e74696d65486f6c64526561736f6e3e602e38546f6f4d616e79467265657a6573000904984e756d626572206f6620667265657a65732065786365656420604d6178467265657a6573602e4c49737375616e63654465616374697661746564000a0401015468652069737375616e63652063616e6e6f74206265206d6f6469666965642073696e636520697420697320616c72656164792064656163746976617465642e2444656c74615a65726f000b04645468652064656c74612063616e6e6f74206265207a65726f2e048054686520604572726f726020656e756d206f6620746869732070616c6c65742e69020c3473705f61726974686d657469632c66697865645f706f696e7424466978656455313238000004002001107531323800006d02086870616c6c65745f7472616e73616374696f6e5f7061796d656e742052656c6561736573000108245631416e6369656e7400000008563200010000710200000408009c007502000004089c0000790200000408ac18007d020000040c00009c008102083c7375627374726174655f6669786564244669786564493132380410467261630185020004011062697473a902011069313238000085020c1c747970656e756d1075696e741055496e74080455018902044201a5020008010c6d7362890201045500010c6c7362a502010442000089020c1c747970656e756d1075696e741055496e74080455018d02044201a5020008010c6d73628d0201045500010c6c7362a50201044200008d020c1c747970656e756d1075696e741055496e74080455019102044201a5020008010c6d7362910201045500010c6c7362a502010442000091020c1c747970656e756d1075696e741055496e74080455019502044201a5020008010c6d7362950201045500010c6c7362a502010442000095020c1c747970656e756d1075696e741055496e74080455019902044201a5020008010c6d7362990201045500010c6c7362a502010442000099020c1c747970656e756d1075696e741055496e74080455019d02044201a1020008010c6d73629d0201045500010c6c7362a10201044200009d020c1c747970656e756d1075696e7414555465726d00000000a1020c1c747970656e756d0c62697408423100000000a5020c1c747970656e756d0c62697408423000000000a9020000050d00ad0200000408000000b102083c7375627374726174655f66697865642446697865645531323804104672616301b5020004011062697473200110753132380000b5020c1c747970656e756d1075696e741055496e74080455018502044201a5020008010c6d7362850201045500010c6c7362a5020104420000b902000004089c9c00bd020000029c00c102000002c50200c5020000040c00181800c9020000022400cd02000002b90200d1020c4070616c6c65745f73756274656e736f721870616c6c65742041786f6e496e666f0000200114626c6f636b18010c75363400011c76657273696f6e10010c753332000108697020011075313238000110706f72749c010c75313600011c69705f74797065080108753800012070726f746f636f6c0801087538000130706c616365686f6c646572310801087538000130706c616365686f6c6465723208010875380000d5020c4070616c6c65745f73756274656e736f721870616c6c6574444e6575726f6e436572746966696361746500000801287075626c69635f6b6579d9020170426f756e6465645665633c75382c20436f6e73745533323c36343e3e000124616c676f726974686d08010875380000d9020c4c626f756e6465645f636f6c6c656374696f6e732c626f756e6465645f76656328426f756e64656456656308045401080453000004003801185665633c543e0000dd020c4070616c6c65745f73756274656e736f721870616c6c65743850726f6d657468657573496e666f0000140114626c6f636b18010c75363400011c76657273696f6e10010c753332000108697020011075313238000110706f72749c010c75313600011c69705f7479706508010875380000e1020c4070616c6c65745f73756274656e736f721870616c6c657434436861696e4964656e7469747900001801106e616d6538011c5665633c75383e00010c75726c38011c5665633c75383e000114696d61676538011c5665633c75383e00011c646973636f726438011c5665633c75383e00012c6465736372697074696f6e38011c5665633c75383e0001286164646974696f6e616c38011c5665633c75383e0000e5020c4070616c6c65745f73756274656e736f721870616c6c65743c436861696e4964656e74697479563200001c01106e616d6538011c5665633c75383e00010c75726c38011c5665633c75383e00012c6769746875625f7265706f38011c5665633c75383e000114696d61676538011c5665633c75383e00011c646973636f726438011c5665633c75383e00012c6465736372697074696f6e38011c5665633c75383e0001286164646974696f6e616c38011c5665633c75383e0000e9020c4070616c6c65745f73756274656e736f721870616c6c6574385375626e65744964656e7469747900000c012c7375626e65745f6e616d6538011c5665633c75383e00012c6769746875625f7265706f38011c5665633c75383e0001387375626e65745f636f6e7461637438011c5665633c75383e0000ed020c4070616c6c65745f73756274656e736f721870616c6c6574405375626e65744964656e74697479563200001c012c7375626e65745f6e616d6538011c5665633c75383e00012c6769746875625f7265706f38011c5665633c75383e0001387375626e65745f636f6e7461637438011c5665633c75383e0001287375626e65745f75726c38011c5665633c75383e00011c646973636f726438011c5665633c75383e00012c6465736372697074696f6e38011c5665633c75383e0001286164646974696f6e616c38011c5665633c75383e0000f1020000040c009c9c00f502000002f90200f902000004103418181800fd02000004089c1800010300000205030005030000040c000903180009030c4c626f756e6465645f636f6c6c656374696f6e732c626f756e6465645f76656328426f756e64656456656308045401080453000004003801185665633c543e00000d030c4070616c6c65745f73756274656e736f721870616c6c65741043616c6c0404540001b82c7365745f776569676874731001186e65747569649c010c7531360001146465737473bd0201205665633c7531363e00011c77656967687473bd0201205665633c7531363e00012c76657273696f6e5f6b657918010c7536340000e821012d2d2d2053657473207468652063616c6c6572207765696768747320666f722074686520696e63656e74697665206d656368616e69736d2e205468652063616c6c2063616e20626531016d6164652066726f6d2074686520686f746b6579206163636f756e7420736f20697320706f74656e7469616c6c7920696e7365637572652c20686f77657665722c207468652064616d61676539016f66206368616e67696e672077656967687473206973206d696e696d616c20696620636175676874206561726c792e20546869732066756e6374696f6e20696e636c7564657320616c6c207468654d01636865636b73207468617420746865207061737365642077656967687473206d6565742074686520726571756972656d656e74732e2053746f7265642061732075313673207468657920726570726573656e742d01726174696f6e616c2076616c75657320696e207468652072616e6765205b302c315d2077686963682073756d20746f203120616e642063616e20626520696e746572707265746564206173390170726f626162696c69746965732e2054686520737065636966696320776569676874732064657465726d696e6520686f7720696e666c6174696f6e2070726f70616761746573206f7574776172643c66726f6d207468697320706565722e0019014e6f74653a205468652031362062697420696e74656765727320776569676874732073686f756c6420726570726573656e7420312e3020617320746865206d6178207531362e8901486f77657665722c207468652066756e6374696f6e206e6f726d616c697a657320616c6c20696e74656765727320746f207531365f6d617820616e797761792e2054686973206d65616e732074686174206966207468652073756d206f6620616c6c4501656c656d656e7473206973206c6172676572206f7220736d616c6c6572207468616e2074686520616d6f756e74206f6620656c656d656e7473202a207531365f6d61782c20616c6c20656c656d656e74739477696c6c20626520636f7272656374656420666f72207468697320646576696174696f6e2e001c2320417267733ac02a20606f726967696e603a20283c54206173206672616d655f73797374656d3a3a436f6e6669673e4f726967696e293aec202020202d205468652063616c6c65722c206120686f746b65792077686f2077697368657320746f2073657420746865697220776569676874732e00442a20606e6574756964602028753136293acc092d20546865206e6574776f726b20756964207765206172652073657474696e672074686573652077656967687473206f6e2e00542a206064657374736020285665633c7531363e293ad4092d20546865206564676520656e64706f696e7420666f7220746865207765696768742c20692e652e206a20666f7220775f696a2e005c2a2027776569676874732720285665633c7531363e293aec092d205468652075313620696e746567657220656e636f64656420776569676874732e20496e74657270726574656420617320726174696f6e616ce0090976616c75657320696e207468652072616e6765205b302c315d2e2054686579206d7573742073756d20746f20696e33323a3a4d41582e00602a202776657273696f6e5f6b65792720282075363420293a0d01092d20546865206e6574776f726b2076657273696f6e206b657920746f20636865636b206966207468652076616c696461746f7220697320757020746f20646174652e002023204576656e743a342a20576569676874735365743bc0092d204f6e207375636365737366756c6c792073657474696e67207468652077656967687473206f6e20636861696e2e002423205261697365733a6c2a20275375624e6574776f726b446f65734e6f744578697374273adc092d20417474656d7074696e6720746f207365742077656967687473206f6e2061206e6f6e2d6578697374656e74206e6574776f726b2e00482a20274e6f7452656769737465726564273aec092d20417474656d7074696e6720746f2073657420776569676874732066726f6d2061206e6f6e2072656769737465726564206163636f756e742e00682a20275765696768745665634e6f74457175616c53697a65273ae8092d20417474656d7074696e6720746f20736574207765696768747320776974682075696473206e6f74206f662073616d65206c656e6774682e00482a20274475706c696361746555696473273ac4092d20417474656d7074696e6720746f2073657420776569676874732077697468206475706c696361746520756964732e0094202020202a2027556964734c656e67746845786365656455696473496e5375624e6574273ae0092d20417474656d7074696e6720746f2073657420776569676874732061626f766520746865206d617820616c6c6f77656420756964732e00702a2027556964566563436f6e7461696e496e76616c69644f6e65273abc092d20417474656d7074696e6720746f207365742077656967687473207769746820696e76616c696420756964732e00642a20275765696768745665634c656e67746849734c6f77273ae4092d20417474656d7074696e6720746f20736574207765696768747320776974682066657765722077656967687473207468616e206d696e2e00582a20274d61785765696768744578636565646564273af0092d20417474656d7074696e6720746f2073657420776569676874732077697468206d61782076616c756520657863656564696e67206c696d69742e4462617463685f7365745f776569676874730c011c6e657475696473b801445665633c436f6d706163743c7531363e3e00011c77656967687473110301985665633c5665633c28436f6d706163743c7531363e2c20436f6d706163743c7531363e293e3e00013076657273696f6e5f6b6579731d0301445665633c436f6d706163743c7536343e3e0050640d012d2d2d20416c6c6f7773206120686f746b657920746f20736574207765696768747320666f72206d756c7469706c65206e65747569647320617320612062617463682e001c2320417267733ac02a20606f726967696e603a20283c54206173206672616d655f73797374656d3a3a436f6e6669673e4f726967696e293aec202020202d205468652063616c6c65722c206120686f746b65792077686f2077697368657320746f2073657420746865697220776569676874732e00802a20606e6574756964736020285665633c436f6d706163743c7531363e3e293ad0092d20546865206e6574776f726b2075696473207765206172652073657474696e672074686573652077656967687473206f6e2e00d02a2060776569676874736020285665633c5665633c28436f6d706163743c7531363e2c20436f6d706163743c7531363e293e293af0092d20546865207765696768747320746f2073657420666f722065616368206e6574776f726b2e205b287569642c20776569676874292c202e2e2e5d00942a206076657273696f6e5f6b6579736020285665633c436f6d706163743c7536343e3e293a1101092d20546865206e6574776f726b2076657273696f6e206b65797320746f20636865636b206966207468652076616c696461746f7220697320757020746f20646174652e002023204576656e743a342a20576569676874735365743bc0092d204f6e207375636365737366756c6c792073657474696e67207468652077656967687473206f6e20636861696e2e602a20426174636857656967687473436f6d706c657465643b6c092d204f6e2073756363657373206f66207468652062617463682e6c2a204261746368436f6d706c65746564576974684572726f72733bc4092d204f6e206661696c757265206f6620616e79206f6620746865207765696768747320696e207468652062617463682e602a2042617463685765696768744974656d4661696c65643bc0092d204f6e206661696c75726520666f722065616368206661696c6564206974656d20696e207468652062617463682e0038636f6d6d69745f776569676874730801186e65747569649c010c75313600012c636f6d6d69745f686173683401104832353600604c19012d2d2d2d205573656420746f20636f6d6d697420612068617368206f6620796f7572207765696768742076616c75657320746f206c617465722062652072657665616c65642e001c2320417267733aec2a20606f726967696e603a2028603c54206173206672616d655f73797374656d3a3a436f6e6669673e3a3a52756e74696d654f726967696e60293aac20202d20546865207369676e6174757265206f662074686520636f6d6d697474696e6720686f746b65792e004c2a20606e65747569646020286075313660293a7c20202d2054686520753136206e6574776f726b206964656e7469666965722e00642a2060636f6d6d69745f68617368602028604832353660293ac020202d20546865206861736820726570726573656e74696e672074686520636f6d6d697474656420776569676874732e002423205261697365733a642a2060436f6d6d697452657665616c44697361626c6564603a190120202d20417474656d7074696e6720746f20636f6d6d6974207768656e2074686520636f6d6d69742d72657665616c206d656368616e69736d2069732064697361626c65642e00742a2060546f6f4d616e79556e72657665616c6564436f6d6d697473603a750120202d20417474656d7074696e6720746f20636f6d6d6974207768656e20746865207573657220686173206d6f7265207468616e2074686520616c6c6f776564206c696d6974206f6620756e72657665616c656420636f6d6d6974732e005062617463685f636f6d6d69745f7765696768747308011c6e657475696473b801445665633c436f6d706163743c7531363e3e000134636f6d6d69745f686173686573b401245665633c483235363e00645831012d2d2d20416c6c6f7773206120686f746b657920746f20636f6d6d6974207765696768742068617368657320666f72206d756c7469706c65206e65747569647320617320612062617463682e001c2320417267733ac02a20606f726967696e603a20283c54206173206672616d655f73797374656d3a3a436f6e6669673e4f726967696e293aec202020202d205468652063616c6c65722c206120686f746b65792077686f2077697368657320746f2073657420746865697220776569676874732e00802a20606e6574756964736020285665633c436f6d706163743c7531363e3e293ad0092d20546865206e6574776f726b2075696473207765206172652073657474696e672074686573652077656967687473206f6e2e00782a2060636f6d6d69745f6861736865736020285665633c483235363e293a7c092d2054686520636f6d6d69742068617368657320746f20636f6d6d69742e002023204576656e743a342a20576569676874735365743bc0092d204f6e207375636365737366756c6c792073657474696e67207468652077656967687473206f6e20636861696e2e602a20426174636857656967687473436f6d706c657465643b6c092d204f6e2073756363657373206f66207468652062617463682e6c2a204261746368436f6d706c65746564576974684572726f72733bc4092d204f6e206661696c757265206f6620616e79206f6620746865207765696768747320696e207468652062617463682e602a2042617463685765696768744974656d4661696c65643bc0092d204f6e206661696c75726520666f722065616368206661696c6564206974656d20696e207468652062617463682e003872657665616c5f776569676874731401186e65747569649c010c75313600011075696473bd0201205665633c7531363e00011876616c756573bd0201205665633c7531363e00011073616c74bd0201205665633c7531363e00012c76657273696f6e5f6b657918010c75363400619401012d2d2d2d205573656420746f2072657665616c20746865207765696768747320666f7220612070726576696f75736c7920636f6d6d697474656420686173682e001c2320417267733aec2a20606f726967696e603a2028603c54206173206672616d655f73797374656d3a3a436f6e6669673e3a3a52756e74696d654f726967696e60293aa820202d20546865207369676e6174757265206f66207468652072657665616c696e6720686f746b65792e004c2a20606e65747569646020286075313660293a7c20202d2054686520753136206e6574776f726b206964656e7469666965722e00582a206075696473602028605665633c7531363e60293ab020202d20546865207569647320666f72207468652077656967687473206265696e672072657665616c65642e00602a206076616c756573602028605665633c7531363e60293ab420202d205468652076616c756573206f66207468652077656967687473206265696e672072657665616c65642e00582a206073616c74602028605665633c7531363e60293ab820202d205468652073616c74207573656420746f2067656e65726174652074686520636f6d6d697420686173682e00602a206076657273696f6e5f6b65796020286075363460293a7020202d20546865206e6574776f726b2076657273696f6e206b65792e002423205261697365733a642a2060436f6d6d697452657665616c44697361626c6564603a390120202d20417474656d7074696e6720746f2072657665616c2077656967687473207768656e2074686520636f6d6d69742d72657665616c206d656368616e69736d2069732064697361626c65642e00642a20604e6f57656967687473436f6d6d6974466f756e64603af020202d20417474656d7074696e6720746f2072657665616c207765696768747320776974686f757420616e206578697374696e6720636f6d6d69742e00602a206045787069726564576569676874436f6d6d6974603ae820202d20417474656d7074696e6720746f2072657665616c20612077656967687420636f6d6d697420746861742068617320657870697265642e004c2a206052657665616c546f6f4561726c79603a050120202d20417474656d7074696e6720746f2072657665616c2077656967687473206f757473696465207468652076616c69642072657665616c20706572696f642e00902a2060496e76616c696452657665616c436f6d6d6974486173684e6f744d61746368603ae020202d205468652072657665616c6564206861736820646f6573206e6f74206d6174636820616e7920636f6d6d697474656420686173682e004c636f6d6d69745f637276335f776569676874730c01186e65747569649c010c753136000118636f6d6d6974090301d0426f756e6465645665633c75382c20436f6e73745533323c4d41585f435256335f434f4d4d49545f53495a455f42595445533e3e00013072657665616c5f726f756e6418010c75363400637449012d2d2d2d205573656420746f20636f6d6d697420656e6372797074656420636f6d6d69742d72657665616c207633207765696768742076616c75657320746f206c617465722062652072657665616c65642e001c2320417267733aec2a20606f726967696e603a2028603c54206173206672616d655f73797374656d3a3a436f6e6669673e3a3a52756e74696d654f726967696e60293a6820202d2054686520636f6d6d697474696e6720686f746b65792e004c2a20606e65747569646020286075313660293a7c20202d2054686520753136206e6574776f726b206964656e7469666965722e005c2a2060636f6d6d6974602028605665633c75383e60293a9020202d2054686520656e6372797074656420636f6d7072657373656420636f6d6d69742e6c2020202054686520737465707320666f722074686973206172653aa820202020312e20496e7374616e7469617465205b6057656967687473546c6f636b5061796c6f6164605d010120202020322e2053657269616c697a65206974207573696e672074686520607061726974795f7363616c655f636f6465633a3a456e636f646560207472616974750220202020332e20456e637279707420697420666f6c6c6f77696e6720746865207374657073202868657265295b68747470733a2f2f6769746875622e636f6d2f696465616c2d6c6162352f746c652f626c6f622f663865363031396630666230326333383065626661366233306566623631373836646564653037622f74696d656c6f636b2f7372632f746c6f636b2e7273234c3238332d4c3333365ddc20202020202020746f2070726f647563652061205b60544c45436970686572746578743c54696e79424c533338313e605d20747970652e4d0120202020342e2053657269616c697a6520616e6420636f6d7072657373207573696e6720746865206061726b2d73657269616c697a6560206043616e6f6e6963616c53657269616c697a65602074726169742e005c2a2072657665616c5f726f756e6420286075363460293a5d012020202d20546865206472616e642072657665616c20726f756e642077686963682077696c6c206265206176616c6961626c6520647572696e672065706f636820606e2b31602066726f6d207468652063757272656e742c202020202065706f63682e002423205261697365733a6c2a2060436f6d6d697452657665616c563344697361626c6564603a190120202d20417474656d7074696e6720746f20636f6d6d6974207768656e2074686520636f6d6d69742d72657665616c206d656368616e69736d2069732064697361626c65642e00742a2060546f6f4d616e79556e72657665616c6564436f6d6d697473603a750120202d20417474656d7074696e6720746f20636f6d6d6974207768656e20746865207573657220686173206d6f7265207468616e2074686520616c6c6f776564206c696d6974206f6620756e72657665616c656420636f6d6d6974732e005062617463685f72657665616c5f776569676874731401186e65747569649c010c753136000124756964735f6c697374210301345665633c5665633c7531363e3e00012c76616c7565735f6c697374210301345665633c5665633c7531363e3e00012873616c74735f6c697374210301345665633c5665633c7531363e3e00013076657273696f6e5f6b657973510101205665633c7536343e00629cf82d2d2d2d2054686520696d706c656d656e746174696f6e20666f722062617463682072657665616c696e6720636f6d6d697474656420776569676874732e001c2320417267733aec2a20606f726967696e603a2028603c54206173206672616d655f73797374656d3a3a436f6e6669673e3a3a52756e74696d654f726967696e60293aa820202d20546865207369676e6174757265206f66207468652072657665616c696e6720686f746b65792e004c2a20606e65747569646020286075313660293a7c20202d2054686520753136206e6574776f726b206964656e7469666965722e00802a2060756964735f6c697374602028605665633c5665633c7531363e3e60293ae820202d2041206c697374206f66207569647320666f72206561636820736574206f662077656967687473206265696e672072657665616c65642e00882a206076616c7565735f6c697374602028605665633c5665633c7531363e3e60293af020202d2041206c697374206f662076616c75657320666f72206561636820736574206f662077656967687473206265696e672072657665616c65642e00842a206073616c74735f6c697374602028605665633c5665633c7531363e3e60293adc20202d2041206c697374206f662073616c7473207573656420746f2067656e65726174652074686520636f6d6d6974206861736865732e00782a206076657273696f6e5f6b657973602028605665633c7536343e60293a8c20202d2041206c697374206f66206e6574776f726b2076657273696f6e206b6579732e002423205261697365733a642a2060436f6d6d697452657665616c44697361626c6564603a390120202d20417474656d7074696e6720746f2072657665616c2077656967687473207768656e2074686520636f6d6d69742d72657665616c206d656368616e69736d2069732064697361626c65642e00642a20604e6f57656967687473436f6d6d6974466f756e64603af020202d20417474656d7074696e6720746f2072657665616c207765696768747320776974686f757420616e206578697374696e6720636f6d6d69742e00602a206045787069726564576569676874436f6d6d6974603ae820202d20417474656d7074696e6720746f2072657665616c20612077656967687420636f6d6d697420746861742068617320657870697265642e004c2a206052657665616c546f6f4561726c79603a050120202d20417474656d7074696e6720746f2072657665616c2077656967687473206f757473696465207468652076616c69642072657665616c20706572696f642e00902a2060496e76616c696452657665616c436f6d6d6974486173684e6f744d61746368603ae020202d205468652072657665616c6564206861736820646f6573206e6f74206d6174636820616e7920636f6d6d697474656420686173682e00602a2060496e76616c6964496e7075744c656e67746873603ac020202d2054686520696e70757420766563746f727320617265206f66206d69736d617463686564206c656e677468732e3c7365745f74616f5f776569676874731401186e65747569649c010c753136000118686f746b6579000130543a3a4163636f756e7449640001146465737473bd0201205665633c7531363e00011c77656967687473bd0201205665633c7531363e00012c76657273696f6e5f6b657918010c7536340008f01c2320417267733ac02a20606f726967696e603a20283c54206173206672616d655f73797374656d3a3a436f6e6669673e4f726967696e293ae0092d205468652063616c6c65722c206120686f746b65792077686f2077697368657320746f2073657420746865697220776569676874732e00442a20606e6574756964602028753136293acc092d20546865206e6574776f726b20756964207765206172652073657474696e672074686573652077656967687473206f6e2e00682a2060686f746b6579602028543a3a4163636f756e744964293a1101092d2054686520686f746b6579206173736f636961746564207769746820746865206f7065726174696f6e20616e64207468652063616c6c696e6720636f6c646b65792e00542a206064657374736020285665633c7531363e293ad4092d20546865206564676520656e64706f696e7420666f7220746865207765696768742c20692e652e206a20666f7220775f696a2e005c2a2027776569676874732720285665633c7531363e293aec092d205468652075313620696e746567657220656e636f64656420776569676874732e20496e74657270726574656420617320726174696f6e616ce0090976616c75657320696e207468652072616e6765205b302c315d2e2054686579206d7573742073756d20746f20696e33323a3a4d41582e00602a202776657273696f6e5f6b65792720282075363420293a0d01092d20546865206e6574776f726b2076657273696f6e206b657920746f20636865636b206966207468652076616c696461746f7220697320757020746f20646174652e002023204576656e743a00342a20576569676874735365743bc0092d204f6e207375636365737366756c6c792073657474696e67207468652077656967687473206f6e20636861696e2e002423205261697365733a005c2a204e6f6e4173736f636961746564436f6c644b65793be8092d20417474656d7074696e6720746f207365742077656967687473206f6e2061206e6f6e2d6173736f63696174656420636f6c64206b65792e006c2a20275375624e6574776f726b446f65734e6f744578697374273adc092d20417474656d7074696e6720746f207365742077656967687473206f6e2061206e6f6e2d6578697374656e74206e6574776f726b2e00482a20274e6f74526f6f745375626e6574273a1901092d20417474656d7074696e6720746f207365742077656967687473206f6e2061207375626e65742074686174206973206e6f742074686520726f6f74206e6574776f726b2e00682a20275765696768745665634e6f74457175616c53697a65273ae8092d20417474656d7074696e6720746f20736574207765696768747320776974682075696473206e6f74206f662073616d65206c656e6774682e00702a2027556964566563436f6e7461696e496e76616c69644f6e65273abc092d20417474656d7074696e6720746f207365742077656967687473207769746820696e76616c696420756964732e00482a20274e6f7452656769737465726564273aec092d20417474656d7074696e6720746f2073657420776569676874732066726f6d2061206e6f6e2072656769737465726564206163636f756e742e00642a20275765696768745665634c656e67746849734c6f77273ae4092d20417474656d7074696e6720746f20736574207765696768747320776974682066657765722077656967687473207468616e206d696e2e007c202a2027496e636f727265637457656967687456657273696f6e4b6579273a210120202020202d20417474656d7074696e6720746f20736574207765696768747320776974682074686520696e636f7272656374206e6574776f726b2076657273696f6e206b65792e006c202a202753657474696e6757656967687473546f6f46617374273aa820202020202d20417474656d7074696e6720746f20736574207765696768747320746f6f20666173742e00642a20275765696768745665634c656e67746849734c6f77273ae4092d20417474656d7074696e6720746f20736574207765696768747320776974682066657765722077656967687473207468616e206d696e2e00582a20274d61785765696768744578636565646564273af0092d20417474656d7074696e6720746f2073657420776569676874732077697468206d61782076616c756520657863656564696e67206c696d69742e003c6265636f6d655f64656c6567617465040118686f746b6579000130543a3a4163636f756e74496400015c7c2d2d2d205365747320746865206b657920617320612064656c65676174652e001c2320417267733ac02a20276f726967696e273a20283c54206173206672616d655f73797374656d3a3a436f6e6669673e4f726967696e293aa4092d20546865207369676e6174757265206f66207468652063616c6c6572277320636f6c646b65792e00682a2027686f746b6579272028543a3a4163636f756e744964293afc092d2054686520686f746b6579207765206172652064656c65676174696e6720286d757374206265206f776e65642062792074686520636f6c646b65792e29003c2a202774616b65272028753634293a0101092d20546865207374616b652070726f706f7274696f6e2074686174207468697320686f746b65792074616b65732066726f6d2064656c65676174696f6e732e002023204576656e743a402a2044656c656761746541646465643bc8092d204f6e207375636365737366756c6c792073657474696e67206120686f746b657920617320612064656c65676174652e002423205261697365733a482a20274e6f7452656769737465726564273a0501092d2054686520686f746b6579207765206172652064656c65676174696e67206973206e6f742072656769737465726564206f6e20746865206e6574776f726b2e00642a20274e6f6e4173736f636961746564436f6c644b6579273a1101092d2054686520686f746b6579207765206172652064656c65676174696e67206973206e6f74206f776e6564206279207468652063616c6c696e6720636f6c646b65742e003464656372656173655f74616b65080118686f746b6579000130543a3a4163636f756e74496400011074616b659c010c753136004184c02d2d2d20416c6c6f77732064656c65676174657320746f206465637265617365206974732074616b652076616c75652e001c2320417267733ac82a20276f726967696e273a20283c54206173206672616d655f73797374656d3a3a436f6e6669673e3a3a4f726967696e293aa4092d20546865207369676e6174757265206f66207468652063616c6c6572277320636f6c646b65792e00682a2027686f746b6579272028543a3a4163636f756e744964293afc092d2054686520686f746b6579207765206172652064656c65676174696e6720286d757374206265206f776e65642062792074686520636f6c646b65792e2900442a20276e6574756964272028753136293a84092d205375626e657420494420746f2064656372656173652074616b6520666f72003c2a202774616b65272028753136293a1101092d20546865206e6577207374616b652070726f706f7274696f6e2074686174207468697320686f746b65792074616b65732066726f6d2064656c65676174696f6e732e1d0120202020202020546865206e65772076616c75652063616e206265206265747765656e203020616e642031315f37393620616e642073686f756c64206265207374726963746c793901202020202020206c6f776572207468616e207468652070726576696f75732076616c75652e204974205420697320746865206e65772076616c75652028726174696f6e616c206e756d626572292c3d01202020202020207468652074686520706172616d657465722069732063616c63756c61746564206173205b3635353335202a20545d2e20466f72206578616d706c652c20312520776f756c6420626598202020202020205b302e3031202a2036353533355d203d205b3635352e33355d203d20363535002023204576656e743a402a2054616b654465637265617365643bf0092d204f6e207375636365737366756c6c792073657474696e672061206465637265617365642074616b6520666f72207468697320686f746b65792e002423205261697365733a482a20274e6f7452656769737465726564273a0501092d2054686520686f746b6579207765206172652064656c65676174696e67206973206e6f742072656769737465726564206f6e20746865206e6574776f726b2e00642a20274e6f6e4173736f636961746564436f6c644b6579273a1101092d2054686520686f746b6579207765206172652064656c65676174696e67206973206e6f74206f776e6564206279207468652063616c6c696e6720636f6c646b65792e005c2a202744656c656761746554616b65546f6f4c6f77273a1d01092d205468652064656c65676174652069732073657474696e6720612074616b65207768696368206973206e6f74206c6f776572207468616e207468652070726576696f75732e0034696e6372656173655f74616b65080118686f746b6579000130543a3a4163636f756e74496400011074616b659c010c7531360042782d012d2d2d20416c6c6f77732064656c65676174657320746f20696e637265617365206974732074616b652076616c75652e20546869732063616c6c20697320726174652d6c696d697465642e001c2320417267733ac82a20276f726967696e273a20283c54206173206672616d655f73797374656d3a3a436f6e6669673e3a3a4f726967696e293aa4092d20546865207369676e6174757265206f66207468652063616c6c6572277320636f6c646b65792e00682a2027686f746b6579272028543a3a4163636f756e744964293afc092d2054686520686f746b6579207765206172652064656c65676174696e6720286d757374206265206f776e65642062792074686520636f6c646b65792e29003c2a202774616b65272028753136293a1101092d20546865206e6577207374616b652070726f706f7274696f6e2074686174207468697320686f746b65792074616b65732066726f6d2064656c65676174696f6e732e1d0120202020202020546865206e65772076616c75652063616e206265206265747765656e203020616e642031315f37393620616e642073686f756c64206265207374726963746c7935012020202020202067726561746572207468616e207468652070726576696f75732076616c75652e205420697320746865206e65772076616c75652028726174696f6e616c206e756d626572292c3d01202020202020207468652074686520706172616d657465722069732063616c63756c61746564206173205b3635353335202a20545d2e20466f72206578616d706c652c20312520776f756c6420626598202020202020205b302e3031202a2036353533355d203d205b3635352e33355d203d20363535002023204576656e743a402a2054616b65496e637265617365643bf0092d204f6e207375636365737366756c6c792073657474696e67206120696e637265617365642074616b6520666f72207468697320686f746b65792e002423205261697365733a482a20274e6f7452656769737465726564273a0501092d2054686520686f746b6579207765206172652064656c65676174696e67206973206e6f742072656769737465726564206f6e20746865206e6574776f726b2e00642a20274e6f6e4173736f636961746564436f6c644b6579273a1101092d2054686520686f746b6579207765206172652064656c65676174696e67206973206e6f74206f776e6564206279207468652063616c6c696e6720636f6c646b65792e00602a202744656c656761746554616b65546f6f48696768273a2501092d205468652064656c65676174652069732073657474696e6720612074616b65207768696368206973206e6f742067726561746572207468616e207468652070726576696f75732e00246164645f7374616b650c0118686f746b6579000130543a3a4163636f756e7449640001186e65747569649c010c753136000134616d6f756e745f7374616b656418010c753634000278d42d2d2d2041646473207374616b6520746f206120686f746b65792e205468652063616c6c206973206d6164652066726f6d2074686594636f6c646b6579206163636f756e74206c696e6b656420696e2074686520686f746b65792ee84f6e6c7920746865206173736f63696174656420636f6c646b657920697320616c6c6f77656420746f206d616b65207374616b696e6720616e64d0756e7374616b696e672072657175657374732e20546869732070726f746563747320746865206e6575726f6e20616761696e7374c461747461636b73206f6e2069747320686f746b65792072756e6e696e6720696e2070726f64756374696f6e20636f64652e001c2320417267733ac4202a20276f726967696e273a20283c54206173206672616d655f73797374656d3a3a436f6e6669673e4f726967696e293aa4092d20546865207369676e6174757265206f66207468652063616c6c6572277320636f6c646b65792e006c202a2027686f746b6579272028543a3a4163636f756e744964293a84092d20546865206173736f63696174656420686f746b6579206163636f756e742e0064202a2027616d6f756e745f7374616b6564272028753634293a0501092d2054686520616d6f756e74206f66207374616b6520746f20626520616464656420746f2074686520686f746b6579207374616b696e67206163636f756e742e002023204576656e743a38202a205374616b6541646465643be0092d204f6e20746865207375636365737366756c6c7920616464696e67207374616b6520746f206120676c6f62616c206163636f756e742e002423205261697365733a74202a20274e6f74456e6f75676842616c616e6365546f5374616b65273a1101092d204e6f7420656e6f7567682062616c616e6365206f6e2074686520636f6c646b657920746f20616464206f6e746f2074686520676c6f62616c206163636f756e742e0068202a20274e6f6e4173736f636961746564436f6c644b6579273ae8092d205468652063616c6c696e6720636f6c646b6579206973206e6f74206173736f6369617465642077697468207468697320686f746b65792e0070202a202742616c616e63655769746864726177616c4572726f72273ab020092d204572726f7273207374656d6d696e672066726f6d207472616e73616374696f6e2070616c6c65742e003072656d6f76655f7374616b650c0118686f746b6579000130543a3a4163636f756e7449640001186e65747569649c010c75313600013c616d6f756e745f756e7374616b656418010c753634000370f052656d6f7665207374616b652066726f6d20746865207374616b696e67206163636f756e742e205468652063616c6c206d757374206265206d6164651d0166726f6d2074686520636f6c646b6579206163636f756e7420617474616368656420746f20746865206e6575726f6e206d657461646174612e204f6e6c792074686973206b6579d8686173207065726d697373696f6e20746f206d616b65207374616b696e6720616e6420756e7374616b696e672072657175657374732e001c2320417267733ac02a20276f726967696e273a20283c54206173206672616d655f73797374656d3a3a436f6e6669673e4f726967696e293aa4092d20546865207369676e6174757265206f66207468652063616c6c6572277320636f6c646b65792e00682a2027686f746b6579272028543a3a4163636f756e744964293a84092d20546865206173736f63696174656420686f746b6579206163636f756e742e00682a2027616d6f756e745f756e7374616b6564272028753634293a0501092d2054686520616d6f756e74206f66207374616b6520746f20626520616464656420746f2074686520686f746b6579207374616b696e67206163636f756e742e002023204576656e743a3c2a205374616b6552656d6f7665643bf8092d204f6e20746865207375636365737366756c6c792072656d6f76696e67207374616b652066726f6d2074686520686f746b6579206163636f756e742e002423205261697365733a482a20274e6f7452656769737465726564273a2d01092d205468726f776e20696620746865206163636f756e742077652061726520617474656d7074696e6720746f20756e7374616b652066726f6d206973206e6f6e206578697374656e742e00642a20274e6f6e4173736f636961746564436f6c644b6579273a1d01092d205468726f776e2069662074686520636f6c646b657920646f6573206e6f74206f776e2074686520686f746b65792077652061726520756e7374616b696e672066726f6d2e00742a20274e6f74456e6f7567685374616b65546f5769746864726177273a3901092d205468726f776e206966207468657265206973206e6f7420656e6f756768207374616b65206f6e2074686520686f746b657920746f20776974686477726177207468697320616d6f756e742e002873657276655f61786f6e2001186e65747569649c010c75313600011c76657273696f6e10010c753332000108697020011075313238000110706f72749c010c75313600011c69705f74797065080108753800012070726f746f636f6c0801087538000130706c616365686f6c646572310801087538000130706c616365686f6c6465723208010875380004cca901536572766573206f7220757064617465732061786f6e202f70726f6d65746865757320696e666f726d6174696f6e20666f7220746865206e6575726f6e206173736f6369617465642077697468207468652063616c6c65722e204966207468652063616c6c6572206973ad01616c7265616479207265676973746572656420746865206d6574616461746120697320757064617465642e204966207468652063616c6c6572206973206e6f74207265676973746572656420746869732063616c6c207468726f7773204e6f74526567697374657265642e001c2320417267733ac02a20276f726967696e273a20283c54206173206672616d655f73797374656d3a3a436f6e6669673e4f726967696e293a7c092d20546865207369676e6174757265206f66207468652063616c6c65722e00442a20276e6574756964272028753136293a78092d2054686520753136206e6574776f726b206964656e7469666965722e00482a202776657273696f6e272028753634293a90092d205468652062697474656e736f722076657273696f6e206964656e7469666965722e00342a20276970272028753634293ae4092d2054686520656e64706f696e7420697020696e666f726d6174696f6e2061732061207531323820656e636f64656420696e74656765722e003c2a2027706f7274272028753136293ae8092d2054686520656e64706f696e7420706f727420696e666f726d6174696f6e20617320612075313620656e636f64656420696e74656765722e00442a202769705f747970652720287538293aac092d2054686520656e64706f696e742069702076657273696f6e20617320612075382c2034206f7220362e00482a202770726f746f636f6c2720287538293a44092d205544503a31206f72205443503a3000582a2027706c616365686f6c646572312720287538293aa0092d20506c616365686f6c64657220666f72206675727468657220657874726120706172616d732e00582a2027706c616365686f6c646572322720287538293aa0092d20506c616365686f6c64657220666f72206675727468657220657874726120706172616d732e002023204576656e743a342a2041786f6e5365727665643ba4092d204f6e207375636365737366756c6c792073657276696e67207468652061786f6e20696e666f2e002423205261697365733a6c2a20275375624e6574776f726b446f65734e6f744578697374273adc092d20417474656d7074696e6720746f207365742077656967687473206f6e2061206e6f6e2d6578697374656e74206e6574776f726b2e00482a20274e6f7452656769737465726564273aec092d20417474656d7074696e6720746f2073657420776569676874732066726f6d2061206e6f6e2072656769737465726564206163636f756e742e00482a2027496e76616c6964497054797065273a74092d205468652069702074797065206973206e6f742034206f7220362e00542a2027496e76616c6964497041646472657373273a1901092d20546865206e756d65726963616c6c7920656e636f646564206970206164647265737320646f6573206e6f74207265736f6c766520746f20612070726f7065722069702e00742a202753657276696e67526174654c696d69744578636565646564273a1d01092d20417474656d7074696e6720746f207365742070726f6d65746865757320696e666f726d6174696f6e2077697468696e67207468652072617465206c696d6974206d696e2e003873657276655f61786f6e5f746c732401186e65747569649c010c75313600011c76657273696f6e10010c753332000108697020011075313238000110706f72749c010c75313600011c69705f74797065080108753800012070726f746f636f6c0801087538000130706c616365686f6c646572310801087538000130706c616365686f6c64657232080108753800012c636572746966696361746538011c5665633c75383e0028dc2d0153616d65206173206073657276655f61786f6e60206275742074616b6573206120636572746966696361746520617320616e206578747261206f7074696f6e616c20617267756d656e742ea901536572766573206f7220757064617465732061786f6e202f70726f6d65746865757320696e666f726d6174696f6e20666f7220746865206e6575726f6e206173736f6369617465642077697468207468652063616c6c65722e204966207468652063616c6c6572206973ad01616c7265616479207265676973746572656420746865206d6574616461746120697320757064617465642e204966207468652063616c6c6572206973206e6f74207265676973746572656420746869732063616c6c207468726f7773204e6f74526567697374657265642e001c2320417267733ac02a20276f726967696e273a20283c54206173206672616d655f73797374656d3a3a436f6e6669673e4f726967696e293a7c092d20546865207369676e6174757265206f66207468652063616c6c65722e00442a20276e6574756964272028753136293a78092d2054686520753136206e6574776f726b206964656e7469666965722e00482a202776657273696f6e272028753634293a90092d205468652062697474656e736f722076657273696f6e206964656e7469666965722e00342a20276970272028753634293ae4092d2054686520656e64706f696e7420697020696e666f726d6174696f6e2061732061207531323820656e636f64656420696e74656765722e003c2a2027706f7274272028753136293ae8092d2054686520656e64706f696e7420706f727420696e666f726d6174696f6e20617320612075313620656e636f64656420696e74656765722e00442a202769705f747970652720287538293aac092d2054686520656e64706f696e742069702076657273696f6e20617320612075382c2034206f7220362e00482a202770726f746f636f6c2720287538293a44092d205544503a31206f72205443503a3000582a2027706c616365686f6c646572312720287538293aa0092d20506c616365686f6c64657220666f72206675727468657220657874726120706172616d732e00582a2027706c616365686f6c646572322720287538293aa0092d20506c616365686f6c64657220666f72206675727468657220657874726120706172616d732e00682a202763657274696669636174652720285665633c75383e293ad4202020202d20544c5320636572746966696361746520666f7220696e746572206e6575726f6e20636f6d6d756e69746174696f6e2e002023204576656e743a342a2041786f6e5365727665643ba4092d204f6e207375636365737366756c6c792073657276696e67207468652061786f6e20696e666f2e002423205261697365733a6c2a20275375624e6574776f726b446f65734e6f744578697374273adc092d20417474656d7074696e6720746f207365742077656967687473206f6e2061206e6f6e2d6578697374656e74206e6574776f726b2e00482a20274e6f7452656769737465726564273aec092d20417474656d7074696e6720746f2073657420776569676874732066726f6d2061206e6f6e2072656769737465726564206163636f756e742e00482a2027496e76616c6964497054797065273a74092d205468652069702074797065206973206e6f742034206f7220362e00542a2027496e76616c6964497041646472657373273a1901092d20546865206e756d65726963616c6c7920656e636f646564206970206164647265737320646f6573206e6f74207265736f6c766520746f20612070726f7065722069702e00742a202753657276696e67526174654c696d69744578636565646564273a1d01092d20417474656d7074696e6720746f207365742070726f6d65746865757320696e666f726d6174696f6e2077697468696e67207468652072617465206c696d6974206d696e2e004073657276655f70726f6d6574686575731401186e65747569649c010c75313600011c76657273696f6e10010c753332000108697020011075313238000110706f72749c010c75313600011c69705f747970650801087538000550bc2d2d2d2d205365742070726f6d65746865757320696e666f726d6174696f6e20666f7220746865206e6575726f6e2e1c2320417267733ac02a20276f726967696e273a20283c54206173206672616d655f73797374656d3a3a436f6e6669673e4f726967696e293a9c092d20546865207369676e6174757265206f66207468652063616c6c696e6720686f746b65792e00442a20276e6574756964272028753136293a78092d2054686520753136206e6574776f726b206964656e7469666965722e00482a202776657273696f6e272028753136293a94092d20205468652062697474656e736f722076657273696f6e206964656e7469666965722e00382a2027697027202875313238293aec092d205468652070726f6d65746865757320697020696e666f726d6174696f6e2061732061207531323820656e636f64656420696e74656765722e003c2a2027706f7274272028753136293af0092d205468652070726f6d65746865757320706f727420696e666f726d6174696f6e20617320612075313620656e636f64656420696e74656765722e00442a202769705f747970652720287538293a60092d205468652069702074797065207634206f722076362e002072656769737465721801186e65747569649c010c753136000130626c6f636b5f6e756d62657218010c7536340001146e6f6e636518010c753634000110776f726b38011c5665633c75383e000118686f746b6579000130543a3a4163636f756e74496400011c636f6c646b6579000130543a3a4163636f756e7449640006bcb82d2d2d2d205265676973746572732061206e6577206e6575726f6e20746f20746865207375626e6574776f726b2e001c2320417267733ac02a20276f726967696e273a20283c54206173206672616d655f73797374656d3a3a436f6e6669673e4f726967696e293a9c092d20546865207369676e6174757265206f66207468652063616c6c696e6720686f746b65792e00442a20276e6574756964272028753136293a78092d2054686520753136206e6574776f726b206964656e7469666965722e00642a2027626c6f636b5f6e756d6265722720282075363420293a98092d20426c6f636b2068617368207573656420746f2070726f766520776f726b20646f6e652e00482a20276e6f6e63652720282075363420293a98092d20506f73697469766520696e7465676572206e6f6e6365207573656420696e20504f572e00542a2027776f726b272028205665633c75383e20293abc092d20566563746f7220656e636f64656420627974657320726570726573656e74696e6720776f726b20646f6e652e00702a2027686f746b657927202820543a3a4163636f756e74496420293aa8092d20486f746b657920746f206265207265676973746572656420746f20746865206e6574776f726b2e00742a2027636f6c646b657927202820543a3a4163636f756e74496420293a78092d204173736f63696174656420636f6c646b6579206163636f756e742e002023204576656e743a4c2a204e6575726f6e526567697374657265643b1901092d204f6e207375636365737366756c6c79207265676973746572696e6720612075696420746f2061206e6575726f6e20736c6f74206f6e2061207375626e6574776f726b2e002423205261697365733a6c2a20275375624e6574776f726b446f65734e6f744578697374273ad0092d20417474656d7074696e6720746f20726567697374657220746f2061206e6f6e206578697374656e74206e6574776f726b2e00882a2027546f6f4d616e79526567697374726174696f6e7354686973426c6f636b273a2901092d205468697320726567697374726174696f6e20657863656564732074686520746f74616c20616c6c6f776564206f6e2074686973206e6574776f726b207468697320626c6f636b2e00902a2027486f744b6579416c726561647952656769737465726564496e5375624e6574273ad0092d2054686520686f746b657920697320616c72656164792072656769737465726564206f6e2074686973206e6574776f726b2e00542a2027496e76616c6964576f726b426c6f636b273a2501092d2054686520776f726b20686173206265656e20706572666f726d6564206f6e2061207374616c652c206675747572652c206f72206e6f6e206578697374656e7420626c6f636b2e00582a2027496e76616c6964446966666963756c7479273aa8092d2054686520776f726b20646f6573206e6f74206d617463682074686520646966666963756c74792e00402a2027496e76616c69645365616c273a64092d20546865207365616c20697320696e636f72726563742e0034726f6f745f7265676973746572040118686f746b6579000130543a3a4163636f756e744964003e048c52656769737465722074686520686f746b657920746f20726f6f74206e6574776f726b3461646a7573745f73656e617465040118686f746b6579000130543a3a4163636f756e744964003f04ec417474656d707420746f2061646a757374207468652073656e617465206d656d6265727368697020746f20696e636c756465206120686f746b65793c6275726e65645f72656769737465720801186e65747569649c010c753136000118686f746b6579000130543a3a4163636f756e744964000704c0557365722072656769737465722061206e6577207375626e6574776f726b20766961206275726e696e6720746f6b656e2c737761705f686f746b6579080118686f746b6579000130543a3a4163636f756e7449640001286e65775f686f746b6579000130543a3a4163636f756e744964004604ac5468652065787472696e73696320666f72207573657220746f206368616e67652069747320686f746b657930737761705f636f6c646b65790c012c6f6c645f636f6c646b6579000130543a3a4163636f756e74496400012c6e65775f636f6c646b6579000130543a3a4163636f756e744964000124737761705f636f737418010c75363400473c2d015468652065787472696e73696320666f72207573657220746f206368616e67652074686520636f6c646b6579206173736f6369617465642077697468207468656972206163636f756e742e002c2320417267756d656e7473001d012a20606f726967696e60202d20546865206f726967696e206f66207468652063616c6c2c206d757374206265207369676e656420627920746865206f6c6420636f6c646b65792e09012a20606f6c645f636f6c646b657960202d205468652063757272656e7420636f6c646b6579206173736f636961746564207769746820746865206163636f756e742e11012a20606e65775f636f6c646b657960202d20546865206e657720636f6c646b657920746f206265206173736f636961746564207769746820746865206163636f756e742e0024232052657475726e7300590152657475726e73206120604469737061746368526573756c7457697468506f7374496e666f6020696e6469636174696e672073756363657373206f72206661696c757265206f6620746865206f7065726174696f6e2e002023205765696768740019015765696768742069732063616c63756c61746564206261736564206f6e20746865206e756d626572206f6620646174616261736520726561647320616e64207772697465732e447365745f6368696c646b65795f74616b650c0118686f746b6579000130543a3a4163636f756e7449640001186e65747569649c010c75313600011074616b659c010c753136004b74a85365747320746865206368696c646b65792074616b6520666f72206120676976656e20686f746b65792e002d01546869732066756e6374696f6e20616c6c6f7773206120636f6c646b657920746f2073657420746865206368696c646b65792074616b6520666f72206120676976656e20686f746b65792e5501546865206368696c646b65792074616b652064657465726d696e6573207468652070726f706f7274696f6e206f66207374616b6520746861742074686520686f746b6579206b6565707320666f7220697473656c66a07768656e20646973747269627574696e67207374616b6520746f20697473206368696c6472656e2e00302320417267756d656e74733ae02a20606f726967696e6020283c54206173206672616d655f73797374656d3a3a436f6e6669673e3a3a52756e74696d654f726967696e293a8901202020202d20546865207369676e6174757265206f66207468652063616c6c696e6720636f6c646b65792e2053657474696e67206368696c646b65792074616b652063616e206f6e6c7920626520646f6e652062792074686520636f6c646b65792e00682a2060686f746b6579602028543a3a4163636f756e744964293ae4202020202d2054686520686f746b657920666f7220776869636820746865206368696c646b65792074616b652077696c6c206265207365742e003c2a206074616b65602028753136293a8d01202020202d20546865206e6577206368696c646b65792074616b652076616c75652e205468697320697320612070657263656e7461676520726570726573656e74656420617320612076616c7565206265747765656e203020616e642031303030302c88202020202020776865726520313030303020726570726573656e747320313030252e002423204576656e74733a502a20604368696c646b657954616b65536574603af4202020202d204f6e207375636365737366756c6c792073657474696e6720746865206368696c646b65792074616b6520666f72206120686f746b65792e002423204572726f72733a642a20604e6f6e4173736f636961746564436f6c644b6579603aa8202020202d2054686520636f6c646b657920646f6573206e6f74206f776e2074686520686f746b65792e602a2060496e76616c69644368696c646b657954616b65603a4501202020202d205468652070726f76696465642074616b652076616c756520697320696e76616c6964202867726561746572207468616e20746865206d6178696d756d20616c6c6f7765642074616b65292e902a206054784368696c646b657954616b65526174654c696d69744578636565646564603a0901202020202d205468652072617465206c696d697420666f72206368616e67696e67206368696c646b65792074616b6520686173206265656e2065786365656465642e00907375646f5f7365745f74785f6368696c646b65795f74616b655f726174655f6c696d697404013474785f726174655f6c696d697418010c75363400452cec5365747320746865207472616e73616374696f6e2072617465206c696d697420666f72206368616e67696e67206368696c646b65792074616b652e00d0546869732066756e6374696f6e2063616e206f6e6c792062652063616c6c65642062792074686520726f6f74206f726967696e2e00302320417267756d656e74733ac82a20606f726967696e60202d20546865206f726967696e206f66207468652063616c6c2c206d75737420626520726f6f742ec42a206074785f726174655f6c696d697460202d20546865206e65772072617465206c696d697420696e20626c6f636b732e002423204572726f72733aa82a20604261644f726967696e60202d20496620746865206f726967696e206973206e6f7420726f6f742e00687375646f5f7365745f6d696e5f6368696c646b65795f74616b6504011074616b659c010c753136004c2c9c5365747320746865206d696e696d756d20616c6c6f776564206368696c646b65792074616b652e00d0546869732066756e6374696f6e2063616e206f6e6c792062652063616c6c65642062792074686520726f6f74206f726967696e2e00302320417267756d656e74733ac82a20606f726967696e60202d20546865206f726967696e206f66207468652063616c6c2c206d75737420626520726f6f742ebc2a206074616b6560202d20546865206e6577206d696e696d756d206368696c646b65792074616b652076616c75652e002423204572726f72733aa82a20604261644f726967696e60202d20496620746865206f726967696e206973206e6f7420726f6f742e00687375646f5f7365745f6d61785f6368696c646b65795f74616b6504011074616b659c010c753136004d2c9c5365747320746865206d6178696d756d20616c6c6f776564206368696c646b65792074616b652e00d0546869732066756e6374696f6e2063616e206f6e6c792062652063616c6c65642062792074686520726f6f74206f726967696e2e00302320417267756d656e74733ac82a20606f726967696e60202d20546865206f726967696e206f66207468652063616c6c2c206d75737420626520726f6f742ebc2a206074616b6560202d20546865206e6577206d6178696d756d206368696c646b65792074616b652076616c75652e002423204572726f72733aa82a20604261644f726967696e60202d20496620746865206f726967696e206973206e6f7420726f6f742e00107375646f04011063616c6c2503015c426f783c543a3a5375646f52756e74696d6543616c6c3e0033184d0141757468656e74696361746573206120636f756e63696c2070726f706f73616c20616e64206469737061746368657320612066756e6374696f6e2063616c6c20776974682060526f6f7460206f726967696e2e00f4546865206469737061746368206f726967696e20666f7220746869732063616c6c206d757374206265206120636f756e63696c206d616a6f726974792e0034232320436f6d706c65786974791c2d204f2831292e547375646f5f756e636865636b65645f77656967687408011063616c6c2503015c426f783c543a3a5375646f52756e74696d6543616c6c3e0001187765696768742c01185765696768740034204d0141757468656e74696361746573206120636f756e63696c2070726f706f73616c20616e64206469737061746368657320612066756e6374696f6e2063616c6c20776974682060526f6f7460206f726967696e2e2d01546869732066756e6374696f6e20646f6573206e6f7420636865636b2074686520776569676874206f66207468652063616c6c2c20616e6420696e737465616420616c6c6f7773207468659c7573657220746f20737065636966792074686520776569676874206f66207468652063616c6c2e00f4546865206469737061746368206f726967696e20666f7220746869732063616c6c206d757374206265206120636f756e63696c206d616a6f726974792e0034232320436f6d706c65786974791c2d204f2831292e10766f7465100118686f746b6579000130543a3a4163636f756e74496400012070726f706f73616c34011c543a3a48617368000114696e6465786101010c75333200011c617070726f7665240110626f6f6c0037045c5573657220766f7465206f6e20612070726f706f73616c4072656769737465725f6e6574776f726b040118686f746b6579000130543a3a4163636f756e744964003b0478557365722072656769737465722061206e6577207375626e6574776f726b186661756365740c0130626c6f636b5f6e756d62657218010c7536340001146e6f6e636518010c753634000110776f726b38011c5665633c75383e003c0cd0466163696c6974792065787472696e73696320666f72207573657220746f206765742074616b656e2066726f6d20666175636574d04974206973206f6e6c7920617661696c61626c65207768656e20706f772d666175636574206665617475726520656e61626c6564dc4a757374206465706c6f79656420696e20746573746e657420616e64206465766e657420666f722074657374696e6720707572706f736540646973736f6c76655f6e6574776f726b08011c636f6c646b6579000130543a3a4163636f756e7449640001186e65747569649c010c753136003d086852656d6f7665206120757365722773207375626e6574776f726bac5468652063616c6c6572206d75737420626520746865206f776e6572206f6620746865206e6574776f726b307365745f6368696c6472656e0c0118686f746b6579000130543a3a4163636f756e7449640001186e65747569649c010c7531360001206368696c6472656eac01605665633c287536342c20543a3a4163636f756e744964293e0043b0f453657420612073696e676c65206368696c6420666f72206120676976656e20686f746b6579206f6e206120737065636966696564206e6574776f726b2e007d01546869732066756e6374696f6e20616c6c6f7773206120636f6c646b657920746f2073657420612073696e676c65206368696c6420666f72206120676976656e20686f746b6579206f6e206120737065636966696564206e6574776f726b2e51015468652070726f706f7274696f6e206f662074686520686f746b65792773207374616b6520746f20626520616c6c6f636174656420746f20746865206368696c6420697320616c736f207370656369666965642e00302320417267756d656e74733ae02a20606f726967696e6020283c54206173206672616d655f73797374656d3a3a436f6e6669673e3a3a52756e74696d654f726967696e293a8d01202020202d20546865207369676e6174757265206f66207468652063616c6c696e6720636f6c646b65792e2053657474696e67206120686f746b6579206368696c642063616e206f6e6c7920626520646f6e652062792074686520636f6c646b65792e00682a2060686f746b6579602028543a3a4163636f756e744964293ac8202020202d2054686520686f746b65792077686963682077696c6c2062652061737369676e656420746865206368696c642e00642a20606368696c64602028543a3a4163636f756e744964293ad4202020202d20546865206368696c642077686963682077696c6c2062652061737369676e656420746f2074686520686f746b65792e00442a20606e6574756964602028753136293afc202020202d2054686520753136206e6574776f726b206964656e74696669657220776865726520746865206368696c646b65792077696c6c2065786973742e00542a206070726f706f7274696f6e602028753634293a8901202020202d2050726f706f7274696f6e206f662074686520686f746b65792773207374616b6520746f20626520676976656e20746f20746865206368696c642c207468652076616c7565206d75737420626520753634206e6f726d616c697a65642e002423204576656e74733a5c2a20604368696c64416464656453696e67756c6172603ad8202020202d204f6e207375636365737366756c6c79207265676973746572696e672061206368696c6420746f206120686f746b65792e002423204572726f72733a6c2a20605375624e6574776f726b446f65734e6f744578697374603adc202020202d20417474656d7074696e6720746f20726567697374657220746f2061206e6f6e2d6578697374656e74206e6574776f726b2ea42a2060526567697374726174696f6e4e6f745065726d69747465644f6e526f6f745375626e6574603ae4202020202d20417474656d7074696e6720746f2072656769737465722061206368696c64206f6e2074686520726f6f74206e6574776f726b2e642a20604e6f6e4173736f636961746564436f6c644b6579603a4501202020202d2054686520636f6c646b657920646f6573206e6f74206f776e2074686520686f746b6579206f7220746865206368696c64206973207468652073616d652061732074686520686f746b65792e6c2a2060486f744b65794163636f756e744e6f74457869737473603aa0202020202d2054686520686f746b6579206163636f756e7420646f6573206e6f742065786973742e0084232044657461696c6564204578706c616e6174696f6e206f6620436865636b733aa501312e202a2a5369676e617475726520566572696669636174696f6e2a2a3a20456e73757265732074686174207468652063616c6c657220686173207369676e656420746865207472616e73616374696f6e2c20766572696679696e672074686520636f6c646b65792ef901322e202a2a526f6f74204e6574776f726b20436865636b2a2a3a20456e73757265732074686174207468652064656c65676174696f6e206973206e6f74206f6e2074686520726f6f74206e6574776f726b2c206173206368696c6420686f746b65797320617265206e6f742076616c6964206f6e2074686520726f6f742e2901332e202a2a4e6574776f726b204578697374656e636520436865636b2a2a3a20456e737572657320746861742074686520737065636966696564206e6574776f726b206578697374732e2101342e202a2a4f776e65727368697020566572696669636174696f6e2a2a3a20456e737572657320746861742074686520636f6c646b6579206f776e732074686520686f746b65792e5901352e202a2a486f746b6579204163636f756e74204578697374656e636520436865636b2a2a3a20456e737572657320746861742074686520686f746b6579206163636f756e7420616c7265616479206578697374732e5901362e202a2a4368696c642d486f746b65792044697374696e6374696f6e2a2a3a20456e7375726573207468617420746865206368696c64206973206e6f74207468652073616d652061732074686520686f746b65792e6501372e202a2a4f6c64204368696c6472656e20436c65616e75702a2a3a2052656d6f7665732074686520686f746b65792066726f6d2074686520706172656e74206c697374206f6620697473206f6c64206368696c6472656e2ec901382e202a2a4e6577204368696c6472656e2041737369676e6d656e742a2a3a2041737369676e7320746865206e6577206368696c6420746f2074686520686f746b657920616e6420757064617465732074686520706172656e74206c69737420666f7220746865206e6577206368696c642e547363686564756c655f737761705f636f6c646b657904012c6e65775f636f6c646b6579000130543a3a4163636f756e74496400498011015363686564756c6573206120636f6c646b65792073776170206f7065726174696f6e20746f20626520657865637574656420617420612066757475726520626c6f636b2e004901546869732066756e6374696f6e20616c6c6f77732061207573657220746f207363686564756c6520746865207377617070696e67206f6620746865697220636f6c646b657920746f2061206e6577206f6e65490161742061207370656369666965642066757475726520626c6f636b2e205468652073776170206973206e6f7420657865637574656420696d6d6564696174656c7920627574206973207363686564756c65649c746f206f63637572206174207468652073706563696669656420626c6f636b206e756d6265722e002c2320417267756d656e74730065012a20606f726967696e60202d20546865206f726967696e206f66207468652063616c6c2c2077686963682073686f756c64206265207369676e6564206279207468652063757272656e7420636f6c646b6579206f776e65722e59012a20606e65775f636f6c646b657960202d20546865206163636f756e74204944206f6620746865206e657720636f6c646b657920746861742077696c6c207265706c616365207468652063757272656e74206f6e652e25012a20607768656e60202d2054686520626c6f636b206e756d6265722061742077686963682074686520636f6c646b657920737761702073686f756c642062652065786563757465642e0024232052657475726e7300610152657475726e73206120604469737061746368526573756c7457697468506f7374496e666f6020696e6469636174696e67207768657468657220746865207363686564756c696e6720776173207375636365737366756c2e002023204572726f72730094546869732066756e6374696f6e206d61792072657475726e20616e206572726f722069663a6c2a20546865206f726967696e206973206e6f74207369676e65642ef82a20546865207363686564756c696e67206661696c732064756520746f20636f6e666c69637473206f722073797374656d20636f6e73747261696e74732e001c23204e6f7465730071012d205468652061637475616c2073776170206973206e6f7420706572666f726d656420627920746869732066756e6374696f6e2e204974206d6572656c79207363686564756c6573207468652073776170206f7065726174696f6e2e81012d2054686520776569676874206f6620746869732063616c6c2069732073657420746f20612066697865642076616c756520616e64206d6179206e6565642061646a7573746d656e74206261736564206f6e2062656e63686d61726b696e672e00182320544f444f003d012d20496d706c656d656e742070726f706572207765696768742063616c63756c6174696f6e206261736564206f6e2074686520636f6d706c6578697479206f6620746865206f7065726174696f6e2e1d012d20436f6e736964657220616464696e6720636865636b7320746f2070726576656e74207363686564756c696e6720746f6f2066617220696e746f20746865206675747572652e64544f444f3a2042656e63686d61726b20746869732063616c6c647363686564756c655f646973736f6c76655f6e6574776f726b0401186e65747569649c010c753136004a3809015363686564756c652074686520646973736f6c7574696f6e206f662061206e6574776f726b20617420612073706563696669656420626c6f636b206e756d6265722e002c2320417267756d656e74730009012a20606f726967696e60202d20546865206f726967696e206f66207468652063616c6c2c206d757374206265207369676e6564206279207468652073656e6465722ee02a20606e657475696460202d2054686520753136206e6574776f726b206964656e74696669657220746f20626520646973736f6c7665642e0024232052657475726e7300590152657475726e73206120604469737061746368526573756c7457697468506f7374496e666f6020696e6469636174696e672073756363657373206f72206661696c757265206f6620746865206f7065726174696f6e2e002023205765696768740019015765696768742069732063616c63756c61746564206261736564206f6e20746865206e756d626572206f6620646174616261736520726561647320616e64207772697465732e307365745f6964656e746974791c01106e616d6538011c5665633c75383e00010c75726c38011c5665633c75383e00012c6769746875625f7265706f38011c5665633c75383e000114696d61676538011c5665633c75383e00011c646973636f726438011c5665633c75383e00012c6465736372697074696f6e38011c5665633c75383e0001286164646974696f6e616c38011c5665633c75383e004450bc2d2d2d2d205365742070726f6d65746865757320696e666f726d6174696f6e20666f7220746865206e6575726f6e2e1c2320417267733ac02a20276f726967696e273a20283c54206173206672616d655f73797374656d3a3a436f6e6669673e4f726967696e293a9c092d20546865207369676e6174757265206f66207468652063616c6c696e6720686f746b65792e00442a20276e6574756964272028753136293a78092d2054686520753136206e6574776f726b206964656e7469666965722e00482a202776657273696f6e272028753136293a94092d20205468652062697474656e736f722076657273696f6e206964656e7469666965722e00382a2027697027202875313238293aec092d205468652070726f6d65746865757320697020696e666f726d6174696f6e2061732061207531323820656e636f64656420696e74656765722e003c2a2027706f7274272028753136293af0092d205468652070726f6d65746865757320706f727420696e666f726d6174696f6e20617320612075313620656e636f64656420696e74656765722e00442a202769705f747970652720287538293a60092d205468652069702074797065207634206f722076362e004c7365745f7375626e65745f6964656e746974792001186e65747569649c010c75313600012c7375626e65745f6e616d6538011c5665633c75383e00012c6769746875625f7265706f38011c5665633c75383e0001387375626e65745f636f6e7461637438011c5665633c75383e0001287375626e65745f75726c38011c5665633c75383e00011c646973636f726438011c5665633c75383e00012c6465736372697074696f6e38011c5665633c75383e0001286164646974696f6e616c38011c5665633c75383e004e40bc2d2d2d2d2053657420746865206964656e7469747920696e666f726d6174696f6e20666f722061207375626e65742e1c2320417267733acc2a20606f726967696e60202d20283c54206173206672616d655f73797374656d3a3a436f6e6669673e3a3a4f726967696e293a4901202020202d20546865207369676e6174757265206f66207468652063616c6c696e6720636f6c646b65792c207768696368206d75737420626520746865206f776e6572206f6620746865207375626e65742e00442a20606e6574756964602028753136293ac8202020202d2054686520756e69717565206e6574776f726b206964656e746966696572206f6620746865207375626e65742e00682a20607375626e65745f6e616d656020285665633c75383e293a74202020202d20546865206e616d65206f6620746865207375626e65742e00682a20606769746875625f7265706f6020285665633c75383e293a0101202020202d2054686520476974487562207265706f7369746f7279206173736f636961746564207769746820746865207375626e6574206964656e746974792e00742a20607375626e65745f636f6e746163746020285665633c75383e293ab4202020202d2054686520636f6e7461637420696e666f726d6174696f6e20666f7220746865207375626e65742e7872656769737465725f6e6574776f726b5f776974685f6964656e74697479080118686f746b6579000130543a3a4163636f756e7449640001206964656e74697479090601684f7074696f6e3c5375626e65744964656e746974794f6656323e004f0478557365722072656769737465722061206e6577207375626e6574776f726b2c756e7374616b655f616c6c040118686f746b6579000130543a3a4163636f756e74496400536435022d2d2d2d2054686520696d706c656d656e746174696f6e20666f72207468652065787472696e73696320756e7374616b655f616c6c3a2052656d6f76657320616c6c207374616b652066726f6d206120686f746b6579206163636f756e74206163726f737320616c6c207375626e65747320616e642061646473206974206f6e746f206120636f6c646b65792e001c2320417267733acc2a20606f726967696e60202d20283c54206173206672616d655f73797374656d3a3a436f6e6669673e3a3a4f726967696e293ab0202020202d20546865207369676e6174757265206f66207468652063616c6c6572277320636f6c646b65792e00682a2060686f746b6579602028543a3a4163636f756e744964293a90202020202d20546865206173736f63696174656420686f746b6579206163636f756e742e002023204576656e743a3c2a205374616b6552656d6f7665643b0501202020202d204f6e20746865207375636365737366756c6c792072656d6f76696e67207374616b652066726f6d2074686520686f746b6579206163636f756e742e002423205261697365733a482a20604e6f7452656769737465726564603a3901202020202d205468726f776e20696620746865206163636f756e742077652061726520617474656d7074696e6720746f20756e7374616b652066726f6d206973206e6f6e206578697374656e742e00642a20604e6f6e4173736f636961746564436f6c644b6579603a2901202020202d205468726f776e2069662074686520636f6c646b657920646f6573206e6f74206f776e2074686520686f746b65792077652061726520756e7374616b696e672066726f6d2e00742a20604e6f74456e6f7567685374616b65546f5769746864726177603a4101202020202d205468726f776e206966207468657265206973206e6f7420656e6f756768207374616b65206f6e2074686520686f746b657920746f207769746864726177207468697320616d6f756e742e00602a20605478526174654c696d69744578636565646564603ac8202020202d205468726f776e206966206b65792068617320686974207472616e73616374696f6e2072617465206c696d697444756e7374616b655f616c6c5f616c706861040118686f746b6579000130543a3a4163636f756e74496400546435022d2d2d2d2054686520696d706c656d656e746174696f6e20666f72207468652065787472696e73696320756e7374616b655f616c6c3a2052656d6f76657320616c6c207374616b652066726f6d206120686f746b6579206163636f756e74206163726f737320616c6c207375626e65747320616e642061646473206974206f6e746f206120636f6c646b65792e001c2320417267733acc2a20606f726967696e60202d20283c54206173206672616d655f73797374656d3a3a436f6e6669673e3a3a4f726967696e293ab0202020202d20546865207369676e6174757265206f66207468652063616c6c6572277320636f6c646b65792e00682a2060686f746b6579602028543a3a4163636f756e744964293a90202020202d20546865206173736f63696174656420686f746b6579206163636f756e742e002023204576656e743a3c2a205374616b6552656d6f7665643b0501202020202d204f6e20746865207375636365737366756c6c792072656d6f76696e67207374616b652066726f6d2074686520686f746b6579206163636f756e742e002423205261697365733a482a20604e6f7452656769737465726564603a3901202020202d205468726f776e20696620746865206163636f756e742077652061726520617474656d7074696e6720746f20756e7374616b652066726f6d206973206e6f6e206578697374656e742e00642a20604e6f6e4173736f636961746564436f6c644b6579603a2901202020202d205468726f776e2069662074686520636f6c646b657920646f6573206e6f74206f776e2074686520686f746b65792077652061726520756e7374616b696e672066726f6d2e00742a20604e6f74456e6f7567685374616b65546f5769746864726177603a4101202020202d205468726f776e206966207468657265206973206e6f7420656e6f756768207374616b65206f6e2074686520686f746b657920746f207769746864726177207468697320616d6f756e742e00602a20605478526174654c696d69744578636565646564603ac8202020202d205468726f776e206966206b65792068617320686974207472616e73616374696f6e2072617465206c696d6974286d6f76655f7374616b651401346f726967696e5f686f746b6579000130543a3a4163636f756e74496400014864657374696e6174696f6e5f686f746b6579000130543a3a4163636f756e7449640001346f726967696e5f6e65747569649c010c75313600014864657374696e6174696f6e5f6e65747569649c010c753136000130616c7068615f616d6f756e7418010c753634005554f9012d2d2d2d2054686520696d706c656d656e746174696f6e20666f72207468652065787472696e736963206d6f76655f7374616b653a204d6f7665732073706563696669656420616d6f756e74206f66207374616b652066726f6d206120686f746b657920746f20616e6f74686572206163726f7373207375626e6574732e001c2320417267733acc2a20606f726967696e60202d20283c54206173206672616d655f73797374656d3a3a436f6e6669673e3a3a4f726967696e293ab0202020202d20546865207369676e6174757265206f66207468652063616c6c6572277320636f6c646b65792e00842a20606f726967696e5f686f746b6579602028543a3a4163636f756e744964293ab0202020202d2054686520686f746b6579206163636f756e7420746f206d6f7665207374616b652066726f6d2e00982a206064657374696e6174696f6e5f686f746b6579602028543a3a4163636f756e744964293aa8202020202d2054686520686f746b6579206163636f756e7420746f206d6f7665207374616b6520746f2e00842a20606f726967696e5f6e6574756964602028543a3a4163636f756e744964293a9c202020202d20546865207375626e657420494420746f206d6f7665207374616b652066726f6d2e00982a206064657374696e6174696f6e5f6e6574756964602028543a3a4163636f756e744964293a94202020202d20546865207375626e657420494420746f206d6f7665207374616b6520746f2e00802a2060616c7068615f616d6f756e74602028543a3a4163636f756e744964293a94202020202d2054686520616c706861207374616b6520616d6f756e7420746f206d6f76652e00387472616e736665725f7374616b6514014c64657374696e6174696f6e5f636f6c646b6579000130543a3a4163636f756e744964000118686f746b6579000130543a3a4163636f756e7449640001346f726967696e5f6e65747569649c010c75313600014864657374696e6174696f6e5f6e65747569649c010c753136000130616c7068615f616d6f756e7418010c75363400565475015472616e736665727320612073706563696669656420616d6f756e74206f66207374616b652066726f6d206f6e6520636f6c646b657920746f20616e6f746865722c206f7074696f6e616c6c79206163726f7373207375626e6574732c787768696c65206b656570696e67207468652073616d6520686f746b65792e002c2320417267756d656e747365012a20606f726967696e60202d20546865206f726967696e206f6620746865207472616e73616374696f6e2c207768696368206d757374206265207369676e65642062792074686520606f726967696e5f636f6c646b6579602e21012a206064657374696e6174696f6e5f636f6c646b657960202d2054686520636f6c646b657920746f20776869636820746865207374616b65206973207472616e736665727265642ec82a2060686f746b657960202d2054686520686f746b6579206173736f636961746564207769746820746865207374616b652ef42a20606f726967696e5f6e657475696460202d20546865206e6574776f726b2f7375626e657420494420746f206d6f7665207374616b652066726f6d2e71012a206064657374696e6174696f6e5f6e657475696460202d20546865206e6574776f726b2f7375626e657420494420746f206d6f7665207374616b6520746f2028666f722063726f73732d7375626e6574207472616e73666572292ecc2a2060616c7068615f616d6f756e7460202d2054686520616d6f756e74206f66207374616b6520746f207472616e736665722e002023204572726f72735052657475726e7320616e206572726f722069663ac82a20546865206f726967696e206973206e6f74207369676e65642062792074686520636f727265637420636f6c646b65792e7c2a20456974686572207375626e657420646f6573206e6f742065786973742e702a2054686520686f746b657920646f6573206e6f742065786973742e2d012a20546865726520697320696e73756666696369656e74207374616b65206f6e2060286f726967696e5f636f6c646b65792c20686f746b65792c206f726967696e5f6e657475696429602ef42a20546865207472616e7366657220616d6f756e742069732062656c6f7720746865206d696e696d756d207374616b6520726571756972656d656e742e002023204576656e7473bc4d617920656d6974206120605374616b655472616e7366657272656460206576656e74206f6e20737563636573732e28737761705f7374616b65100118686f746b6579000130543a3a4163636f756e7449640001346f726967696e5f6e65747569649c010c75313600014864657374696e6174696f6e5f6e65747569649c010c753136000130616c7068615f616d6f756e7418010c75363400574ca101537761707320612073706563696669656420616d6f756e74206f66207374616b652066726f6d206f6e65207375626e657420746f20616e6f746865722c207768696c65206b656570696e67207468652073616d6520636f6c646b657920616e6420686f746b65792e002c2320417267756d656e74739d012a20606f726967696e60202d20546865206f726967696e206f6620746865207472616e73616374696f6e2c207768696368206d757374206265207369676e65642062792074686520636f6c646b65792074686174206f776e73207468652060686f746b6579602ed42a2060686f746b657960202d2054686520686f746b65792077686f7365207374616b65206973206265696e6720737761707065642e19012a20606f726967696e5f6e657475696460202d20546865206e6574776f726b2f7375626e65742049442066726f6d207768696368207374616b652069732072656d6f7665642e1d012a206064657374696e6174696f6e5f6e657475696460202d20546865206e6574776f726b2f7375626e657420494420746f207768696368207374616b652069732061646465642ebc2a2060616c7068615f616d6f756e7460202d2054686520616d6f756e74206f66207374616b6520746f20737761702e002023204572726f72735052657475726e7320616e206572726f722069663a6d012a20546865207472616e73616374696f6e206973206e6f74207369676e65642062792074686520636f727265637420636f6c646b65792028692e652e2c2060636f6c646b65795f6f776e735f686f746b657960206661696c73292e01012a2045697468657220606f726967696e5f6e657475696460206f72206064657374696e6174696f6e5f6e65747569646020646f6573206e6f742065786973742e702a2054686520686f746b657920646f6573206e6f742065786973742e11012a20546865726520697320696e73756666696369656e74207374616b65206f6e206028636f6c646b65792c20686f746b65792c206f726967696e5f6e657475696429602ee42a20546865207377617020616d6f756e742069732062656c6f7720746865206d696e696d756d207374616b6520726571756972656d656e742e002023204576656e7473ac4d617920656d6974206120605374616b655377617070656460206576656e74206f6e20737563636573732e3c6164645f7374616b655f6c696d6974140118686f746b6579000130543a3a4163636f756e7449640001186e65747569649c010c753136000134616d6f756e745f7374616b656418010c75363400012c6c696d69745f707269636518010c753634000134616c6c6f775f7061727469616c240110626f6f6c00589ce82d2d2d2041646473207374616b6520746f206120686f746b6579206f6e2061207375626e657420776974682061207072696365206c696d69742e0101546869732065787472696e73696320616c6c6f777320746f207370656369667920746865206c696d697420707269636520666f7220616c70686120746f6b656ed86174207768696368206f722062657474657220286c6f7765722920746865207374616b696e672073686f756c6420657865637574652e001101496e206361736520696620736c697070616765206f636375727320616e6420746865207072696365207368616c6c206d6f7665206265796f6e6420746865206c696d6974090170726963652c20746865207374616b696e67206f72646572206d61792065786563757465206f6e6c79207061727469616c6c79206f72206e6f7420657865637574651c617420616c6c2e001c2320417267733ac4202a20276f726967696e273a20283c54206173206672616d655f73797374656d3a3a436f6e6669673e4f726967696e293aa4092d20546865207369676e6174757265206f66207468652063616c6c6572277320636f6c646b65792e006c202a2027686f746b6579272028543a3a4163636f756e744964293a84092d20546865206173736f63696174656420686f746b6579206163636f756e742e0064202a2027616d6f756e745f7374616b6564272028753634293a0501092d2054686520616d6f756e74206f66207374616b6520746f20626520616464656420746f2074686520686f746b6579207374616b696e67206163636f756e742e005c202a20276c696d69745f7072696365272028753634293aec092d20546865206c696d69742070726963652065787072657373656420696e20756e697473206f662052414f20706572206f6e6520416c7068612e0068202a2027616c6c6f775f7061727469616c272028626f6f6c293a2101092d20416c6c6f7773207061727469616c20657865637574696f6e206f662074686520616d6f756e742e2049662073657420746f2066616c73652c2074686973206265636f6d65738420202020202066696c6c206f72206b696c6c2074797065206f72206f726465722e002023204576656e743a38202a205374616b6541646465643be0092d204f6e20746865207375636365737366756c6c7920616464696e67207374616b6520746f206120676c6f62616c206163636f756e742e002423205261697365733a74202a20274e6f74456e6f75676842616c616e6365546f5374616b65273a1101092d204e6f7420656e6f7567682062616c616e6365206f6e2074686520636f6c646b657920746f20616464206f6e746f2074686520676c6f62616c206163636f756e742e0068202a20274e6f6e4173736f636961746564436f6c644b6579273ae8092d205468652063616c6c696e6720636f6c646b6579206973206e6f74206173736f6369617465642077697468207468697320686f746b65792e0070202a202742616c616e63655769746864726177616c4572726f72273ab020092d204572726f7273207374656d6d696e672066726f6d207472616e73616374696f6e2070616c6c65742e004872656d6f76655f7374616b655f6c696d6974140118686f746b6579000130543a3a4163636f756e7449640001186e65747569649c010c75313600013c616d6f756e745f756e7374616b656418010c75363400012c6c696d69745f707269636518010c753634000134616c6c6f775f7061727469616c240110626f6f6c00599cfc2d2d2d2052656d6f766573207374616b652066726f6d206120686f746b6579206f6e2061207375626e657420776974682061207072696365206c696d69742e0101546869732065787472696e73696320616c6c6f777320746f207370656369667920746865206c696d697420707269636520666f7220616c70686120746f6b656edc6174207768696368206f722062657474657220286869676865722920746865207374616b696e672073686f756c6420657865637574652e001101496e206361736520696620736c697070616765206f636375727320616e6420746865207072696365207368616c6c206d6f7665206265796f6e6420746865206c696d6974090170726963652c20746865207374616b696e67206f72646572206d61792065786563757465206f6e6c79207061727469616c6c79206f72206e6f7420657865637574651c617420616c6c2e001c2320417267733ac02a20276f726967696e273a20283c54206173206672616d655f73797374656d3a3a436f6e6669673e4f726967696e293aa4092d20546865207369676e6174757265206f66207468652063616c6c6572277320636f6c646b65792e00682a2027686f746b6579272028543a3a4163636f756e744964293a84092d20546865206173736f63696174656420686f746b6579206163636f756e742e00682a2027616d6f756e745f756e7374616b6564272028753634293a0501092d2054686520616d6f756e74206f66207374616b6520746f20626520616464656420746f2074686520686f746b6579207374616b696e67206163636f756e742e005c202a20276c696d69745f7072696365272028753634293af8202020202d20546865206c696d69742070726963652065787072657373656420696e20756e697473206f662052414f20706572206f6e6520416c7068612e0068202a2027616c6c6f775f7061727469616c272028626f6f6c293a2d01202020202d20416c6c6f7773207061727469616c20657865637574696f6e206f662074686520616d6f756e742e2049662073657420746f2066616c73652c2074686973206265636f6d65738420202020202066696c6c206f72206b696c6c2074797065206f72206f726465722e002023204576656e743a3c2a205374616b6552656d6f7665643bf8092d204f6e20746865207375636365737366756c6c792072656d6f76696e67207374616b652066726f6d2074686520686f746b6579206163636f756e742e002423205261697365733a482a20274e6f7452656769737465726564273a2d01092d205468726f776e20696620746865206163636f756e742077652061726520617474656d7074696e6720746f20756e7374616b652066726f6d206973206e6f6e206578697374656e742e00642a20274e6f6e4173736f636961746564436f6c644b6579273a1d01092d205468726f776e2069662074686520636f6c646b657920646f6573206e6f74206f776e2074686520686f746b65792077652061726520756e7374616b696e672066726f6d2e00742a20274e6f74456e6f7567685374616b65546f5769746864726177273a3901092d205468726f776e206966207468657265206973206e6f7420656e6f756768207374616b65206f6e2074686520686f746b657920746f20776974686477726177207468697320616d6f756e742e0040737761705f7374616b655f6c696d6974180118686f746b6579000130543a3a4163636f756e7449640001346f726967696e5f6e65747569649c010c75313600014864657374696e6174696f6e5f6e65747569649c010c753136000130616c7068615f616d6f756e7418010c75363400012c6c696d69745f707269636518010c753634000134616c6c6f775f7061727469616c240110626f6f6c005a54a101537761707320612073706563696669656420616d6f756e74206f66207374616b652066726f6d206f6e65207375626e657420746f20616e6f746865722c207768696c65206b656570696e67207468652073616d6520636f6c646b657920616e6420686f746b65792e002c2320417267756d656e74739d012a20606f726967696e60202d20546865206f726967696e206f6620746865207472616e73616374696f6e2c207768696368206d757374206265207369676e65642062792074686520636f6c646b65792074686174206f776e73207468652060686f746b6579602ed42a2060686f746b657960202d2054686520686f746b65792077686f7365207374616b65206973206265696e6720737761707065642e19012a20606f726967696e5f6e657475696460202d20546865206e6574776f726b2f7375626e65742049442066726f6d207768696368207374616b652069732072656d6f7665642e1d012a206064657374696e6174696f6e5f6e657475696460202d20546865206e6574776f726b2f7375626e657420494420746f207768696368207374616b652069732061646465642ebc2a2060616c7068615f616d6f756e7460202d2054686520616d6f756e74206f66207374616b6520746f20737761702e29012a20606c696d69745f707269636560202d20546865206c696d69742070726963652065787072657373656420696e20756e697473206f662052414f20706572206f6e6520416c7068612ed5012a2060616c6c6f775f7061727469616c60202d20416c6c6f7773207061727469616c20657865637574696f6e206f662074686520616d6f756e742e2049662073657420746f2066616c73652c2074686973206265636f6d65732066696c6c206f72206b696c6c2074797065206f72206f726465722e002023204572726f72735052657475726e7320616e206572726f722069663a6d012a20546865207472616e73616374696f6e206973206e6f74207369676e65642062792074686520636f727265637420636f6c646b65792028692e652e2c2060636f6c646b65795f6f776e735f686f746b657960206661696c73292e01012a2045697468657220606f726967696e5f6e657475696460206f72206064657374696e6174696f6e5f6e65747569646020646f6573206e6f742065786973742e702a2054686520686f746b657920646f6573206e6f742065786973742e11012a20546865726520697320696e73756666696369656e74207374616b65206f6e206028636f6c646b65792c20686f746b65792c206f726967696e5f6e657475696429602ee42a20546865207377617020616d6f756e742069732062656c6f7720746865206d696e696d756d207374616b6520726571756972656d656e742e002023204576656e7473ac4d617920656d6974206120605374616b655377617070656460206576656e74206f6e20737563636573732e0c6101446973706174636861626c652066756e6374696f6e7320616c6c6f7720757365727320746f20696e7465726163742077697468207468652070616c6c657420616e6420696e766f6b65207374617465206368616e6765732e590154686573652066756e6374696f6e73206d6174657269616c697a65206173202265787472696e73696373222c20776869636820617265206f6674656e20636f6d706172656420746f207472616e73616374696f6e732e6101446973706174636861626c652066756e6374696f6e73206d75737420626520616e6e6f7461746564207769746820612077656967687420616e64206d7573742072657475726e2061204469737061746368526573756c742e11030000021503001503000002190300190300000408bcbc001d0300000230002103000002bd0200250308586e6f64655f73756274656e736f725f72756e74696d652c52756e74696d6543616c6c0001581853797374656d0400690101ad0173656c663a3a73705f6170695f68696464656e5f696e636c756465735f636f6e7374727563745f72756e74696d653a3a68696464656e5f696e636c7564653a3a64697370617463680a3a3a43616c6c61626c6543616c6c466f723c53797374656d2c2052756e74696d653e0000002454696d657374616d700400b10101b90173656c663a3a73705f6170695f68696464656e5f696e636c756465735f636f6e7374727563745f72756e74696d653a3a68696464656e5f696e636c7564653a3a64697370617463680a3a3a43616c6c61626c6543616c6c466f723c54696d657374616d702c2052756e74696d653e0002001c4772616e6470610400d50101b10173656c663a3a73705f6170695f68696464656e5f696e636c756465735f636f6e7374727563745f72756e74696d653a3a68696464656e5f696e636c7564653a3a64697370617463680a3a3a43616c6c61626c6543616c6c466f723c4772616e6470612c2052756e74696d653e0004002042616c616e6365730400510201b50173656c663a3a73705f6170695f68696464656e5f696e636c756465735f636f6e7374727563745f72756e74696d653a3a68696464656e5f696e636c7564653a3a64697370617463680a3a3a43616c6c61626c6543616c6c466f723c42616c616e6365732c2052756e74696d653e0005003c53756274656e736f724d6f64756c6504000d0301d10173656c663a3a73705f6170695f68696464656e5f696e636c756465735f636f6e7374727563745f72756e74696d653a3a68696464656e5f696e636c7564653a3a64697370617463680a3a3a43616c6c61626c6543616c6c466f723c53756274656e736f724d6f64756c652c2052756e74696d653e0007002c547269756d7669726174650400290301c10173656c663a3a73705f6170695f68696464656e5f696e636c756465735f636f6e7374727563745f72756e74696d653a3a68696464656e5f696e636c7564653a3a64697370617463680a3a3a43616c6c61626c6543616c6c466f723c547269756d7669726174652c2052756e74696d653e00080048547269756d7669726174654d656d6265727304002d0301dd0173656c663a3a73705f6170695f68696464656e5f696e636c756465735f636f6e7374727563745f72756e74696d653a3a68696464656e5f696e636c7564653a3a64697370617463680a3a3a43616c6c61626c6543616c6c466f723c547269756d7669726174654d656d626572732c2052756e74696d653e0009003453656e6174654d656d626572730400310301c90173656c663a3a73705f6170695f68696464656e5f696e636c756465735f636f6e7374727563745f72756e74696d653a3a68696464656e5f696e636c7564653a3a64697370617463680a3a3a43616c6c61626c6543616c6c466f723c53656e6174654d656d626572732c2052756e74696d653e000a001c5574696c6974790400350301b10173656c663a3a73705f6170695f68696464656e5f696e636c756465735f636f6e7374727563745f72756e74696d653a3a68696464656e5f696e636c7564653a3a64697370617463680a3a3a43616c6c61626c6543616c6c466f723c5574696c6974792c2052756e74696d653e000b00105375646f04004d0301a50173656c663a3a73705f6170695f68696464656e5f696e636c756465735f636f6e7374727563745f72756e74696d653a3a68696464656e5f696e636c7564653a3a64697370617463680a3a3a43616c6c61626c6543616c6c466f723c5375646f2c2052756e74696d653e000c00204d756c74697369670400510301b50173656c663a3a73705f6170695f68696464656e5f696e636c756465735f636f6e7374727563745f72756e74696d653a3a68696464656e5f696e636c7564653a3a64697370617463680a3a3a43616c6c61626c6543616c6c466f723c4d756c74697369672c2052756e74696d653e000d0020507265696d6167650400590301b50173656c663a3a73705f6170695f68696464656e5f696e636c756465735f636f6e7374727563745f72756e74696d653a3a68696464656e5f696e636c7564653a3a64697370617463680a3a3a43616c6c61626c6543616c6c466f723c507265696d6167652c2052756e74696d653e000e00245363686564756c657204005d0301b90173656c663a3a73705f6170695f68696464656e5f696e636c756465735f636f6e7374727563745f72756e74696d653a3a68696464656e5f696e636c7564653a3a64697370617463680a3a3a43616c6c61626c6543616c6c466f723c5363686564756c65722c2052756e74696d653e000f001450726f78790400650301a90173656c663a3a73705f6170695f68696464656e5f696e636c756465735f636f6e7374727563745f72756e74696d653a3a68696464656e5f696e636c7564653a3a64697370617463680a3a3a43616c6c61626c6543616c6c466f723c50726f78792c2052756e74696d653e00100020526567697374727904006d0301b50173656c663a3a73705f6170695f68696464656e5f696e636c756465735f636f6e7374727563745f72756e74696d653a3a68696464656e5f696e636c7564653a3a64697370617463680a3a3a43616c6c61626c6543616c6c466f723c52656769737472792c2052756e74696d653e0011002c436f6d6d69746d656e74730400790401c10173656c663a3a73705f6170695f68696464656e5f696e636c756465735f636f6e7374727563745f72756e74696d653a3a68696464656e5f696e636c7564653a3a64697370617463680a3a3a43616c6c61626c6543616c6c466f723c436f6d6d69746d656e74732c2052756e74696d653e0012002841646d696e5574696c7304008d0501bd0173656c663a3a73705f6170695f68696464656e5f696e636c756465735f636f6e7374727563745f72756e74696d653a3a68696464656e5f696e636c7564653a3a64697370617463680a3a3a43616c6c61626c6543616c6c466f723c41646d696e5574696c732c2052756e74696d653e00130020536166654d6f64650400910501b50173656c663a3a73705f6170695f68696464656e5f696e636c756465735f636f6e7374727563745f72756e74696d653a3a68696464656e5f696e636c7564653a3a64697370617463680a3a3a43616c6c61626c6543616c6c466f723c536166654d6f64652c2052756e74696d653e00140020457468657265756d0400950501b50173656c663a3a73705f6170695f68696464656e5f696e636c756465735f636f6e7374727563745f72756e74696d653a3a68696464656e5f696e636c7564653a3a64697370617463680a3a3a43616c6c61626c6543616c6c466f723c457468657265756d2c2052756e74696d653e0015000c45564d0400bd0501a10173656c663a3a73705f6170695f68696464656e5f696e636c756465735f636f6e7374727563745f72756e74696d653a3a68696464656e5f696e636c7564653a3a64697370617463680a3a3a43616c6c61626c6543616c6c466f723c45564d2c2052756e74696d653e0016001c426173654665650400d10501b10173656c663a3a73705f6170695f68696464656e5f696e636c756465735f636f6e7374727563745f72756e74696d653a3a68696464656e5f696e636c7564653a3a64697370617463680a3a3a43616c6c61626c6543616c6c466f723c426173654665652c2052756e74696d653e001900144472616e640400d50501a90173656c663a3a73705f6170695f68696464656e5f696e636c756465735f636f6e7374727563745f72756e74696d653a3a68696464656e5f696e636c7564653a3a64697370617463680a3a3a43616c6c61626c6543616c6c466f723c4472616e642c2052756e74696d653e001a000029030c4470616c6c65745f636f6c6c6563746976651870616c6c65741043616c6c0804540004490001182c7365745f6d656d626572730c012c6e65775f6d656d626572735d0201445665633c543a3a4163636f756e7449643e0001147072696d65a801504f7074696f6e3c543a3a4163636f756e7449643e0001246f6c645f636f756e7410012c4d656d626572436f756e74000060805365742074686520636f6c6c6563746976652773206d656d626572736869702e0045012d20606e65775f6d656d62657273603a20546865206e6577206d656d626572206c6973742e204265206e69636520746f2074686520636861696e20616e642070726f7669646520697420736f727465642ee02d20607072696d65603a20546865207072696d65206d656d6265722077686f736520766f74652073657473207468652064656661756c742e59012d20606f6c645f636f756e74603a2054686520757070657220626f756e6420666f72207468652070726576696f7573206e756d626572206f66206d656d6265727320696e2073746f726167652e205573656420666f7250202077656967687420657374696d6174696f6e2e00d4546865206469737061746368206f6620746869732063616c6c206d75737420626520605365744d656d626572734f726967696e602e0051014e4f54453a20446f6573206e6f7420656e666f7263652074686520657870656374656420604d61784d656d6265727360206c696d6974206f6e2074686520616d6f756e74206f66206d656d626572732c2062757421012020202020207468652077656967687420657374696d6174696f6e732072656c79206f6e20697420746f20657374696d61746520646973706174636861626c65207765696768742e002823205741524e494e473a005901546865206070616c6c65742d636f6c6c656374697665602063616e20616c736f206265206d616e61676564206279206c6f676963206f757473696465206f66207468652070616c6c6574207468726f75676820746865b8696d706c656d656e746174696f6e206f6620746865207472616974205b604368616e67654d656d62657273605d2e5501416e792063616c6c20746f20607365745f6d656d6265727360206d757374206265206361726566756c207468617420746865206d656d6265722073657420646f65736e277420676574206f7574206f662073796e63a477697468206f74686572206c6f676963206d616e6167696e6720746865206d656d626572207365742e0038232320436f6d706c65786974793a502d20604f284d50202b204e29602077686572653ae020202d20604d60206f6c642d6d656d626572732d636f756e742028636f64652d20616e6420676f7665726e616e63652d626f756e64656429e020202d20604e60206e65772d6d656d626572732d636f756e742028636f64652d20616e6420676f7665726e616e63652d626f756e646564299820202d206050602070726f706f73616c732d636f756e742028636f64652d626f756e646564291c6578656375746508012070726f706f73616c2503017c426f783c3c5420617320436f6e6669673c493e3e3a3a50726f706f73616c3e0001306c656e6774685f626f756e646101010c753332000124f0446973706174636820612070726f706f73616c2066726f6d2061206d656d626572207573696e672074686520604d656d62657260206f726967696e2e00a84f726967696e206d7573742062652061206d656d626572206f662074686520636f6c6c6563746976652e0038232320436f6d706c65786974793a5c2d20604f2842202b204d202b205029602077686572653ad82d20604260206973206070726f706f73616c602073697a6520696e20627974657320286c656e6774682d6665652d626f756e64656429882d20604d60206d656d626572732d636f756e742028636f64652d626f756e64656429a82d2060506020636f6d706c6578697479206f66206469737061746368696e67206070726f706f73616c601c70726f706f73650c012070726f706f73616c2503017c426f783c3c5420617320436f6e6669673c493e3e3a3a50726f706f73616c3e0001306c656e6774685f626f756e646101010c7533320001206475726174696f6e100144426c6f636b4e756d626572466f723c543e000238f84164642061206e65772070726f706f73616c20746f2065697468657220626520766f746564206f6e206f72206578656375746564206469726563746c792e00845265717569726573207468652073656e64657220746f206265206d656d6265722e004101607468726573686f6c64602064657465726d696e65732077686574686572206070726f706f73616c60206973206578656375746564206469726563746c792028607468726573686f6c64203c20326029546f722070757420757020666f7220766f74696e672e0034232320436f6d706c6578697479ac2d20604f2842202b204d202b2050312960206f7220604f2842202b204d202b20503229602077686572653ae020202d20604260206973206070726f706f73616c602073697a6520696e20627974657320286c656e6774682d6665652d626f756e64656429dc20202d20604d60206973206d656d626572732d636f756e742028636f64652d20616e6420676f7665726e616e63652d626f756e64656429c420202d206272616e6368696e6720697320696e666c75656e63656420627920607468726573686f6c64602077686572653af4202020202d20605031602069732070726f706f73616c20657865637574696f6e20636f6d706c65786974792028607468726573686f6c64203c20326029fc202020202d20605032602069732070726f706f73616c732d636f756e742028636f64652d626f756e646564292028607468726573686f6c64203e3d2032602910766f74650c012070726f706f73616c34011c543a3a48617368000114696e6465786101013450726f706f73616c496e64657800011c617070726f7665240110626f6f6c000324f041646420616e20617965206f72206e617920766f746520666f72207468652073656e64657220746f2074686520676976656e2070726f706f73616c2e008c5265717569726573207468652073656e64657220746f2062652061206d656d6265722e0049015472616e73616374696f6e20666565732077696c6c2062652077616976656420696620746865206d656d62657220697320766f74696e67206f6e20616e7920706172746963756c61722070726f706f73616c5101666f72207468652066697273742074696d6520616e64207468652063616c6c206973207375636365737366756c2e2053756273657175656e7420766f7465206368616e6765732077696c6c206368617267652061106665652e34232320436f6d706c657869747909012d20604f284d296020776865726520604d60206973206d656d626572732d636f756e742028636f64652d20616e6420676f7665726e616e63652d626f756e646564294c646973617070726f76655f70726f706f73616c04013470726f706f73616c5f6861736834011c543a3a486173680005285901446973617070726f766520612070726f706f73616c2c20636c6f73652c20616e642072656d6f76652069742066726f6d207468652073797374656d2c207265676172646c657373206f66206974732063757272656e741873746174652e00884d7573742062652063616c6c65642062792074686520526f6f74206f726967696e2e002c506172616d65746572733a1d012a206070726f706f73616c5f68617368603a205468652068617368206f66207468652070726f706f73616c20746861742073686f756c6420626520646973617070726f7665642e0034232320436f6d706c6578697479ac4f285029207768657265205020697320746865206e756d626572206f66206d61782070726f706f73616c7314636c6f736510013470726f706f73616c5f6861736834011c543a3a48617368000114696e6465786101013450726f706f73616c496e64657800015470726f706f73616c5f7765696768745f626f756e642c01185765696768740001306c656e6774685f626f756e646101010c7533320006604d01436c6f7365206120766f746520746861742069732065697468657220617070726f7665642c20646973617070726f766564206f722077686f736520766f74696e6720706572696f642068617320656e6465642e0055014d61792062652063616c6c656420627920616e79207369676e6564206163636f756e7420696e206f7264657220746f2066696e69736820766f74696e6720616e6420636c6f7365207468652070726f706f73616c2e00490149662063616c6c6564206265666f72652074686520656e64206f662074686520766f74696e6720706572696f642069742077696c6c206f6e6c7920636c6f73652074686520766f7465206966206974206973bc68617320656e6f75676820766f74657320746f20626520617070726f766564206f7220646973617070726f7665642e00490149662063616c6c65642061667465722074686520656e64206f662074686520766f74696e6720706572696f642061627374656e74696f6e732061726520636f756e7465642061732072656a656374696f6e732501756e6c6573732074686572652069732061207072696d65206d656d6265722073657420616e6420746865207072696d65206d656d626572206361737420616e20617070726f76616c2e00610149662074686520636c6f7365206f7065726174696f6e20636f6d706c65746573207375636365737366756c6c79207769746820646973617070726f76616c2c20746865207472616e73616374696f6e206665652077696c6c5d016265207761697665642e204f746865727769736520657865637574696f6e206f662074686520617070726f766564206f7065726174696f6e2077696c6c206265206368617267656420746f207468652063616c6c65722e0061012b206070726f706f73616c5f7765696768745f626f756e64603a20546865206d6178696d756d20616d6f756e74206f662077656967687420636f6e73756d656420627920657865637574696e672074686520636c6f7365642470726f706f73616c2e61012b20606c656e6774685f626f756e64603a2054686520757070657220626f756e6420666f7220746865206c656e677468206f66207468652070726f706f73616c20696e2073746f726167652e20436865636b65642076696135016073746f726167653a3a726561646020736f206974206973206073697a655f6f663a3a3c7533323e2829203d3d203460206c6172676572207468616e207468652070757265206c656e6774682e0034232320436f6d706c6578697479742d20604f2842202b204d202b205031202b20503229602077686572653ae020202d20604260206973206070726f706f73616c602073697a6520696e20627974657320286c656e6774682d6665652d626f756e64656429dc20202d20604d60206973206d656d626572732d636f756e742028636f64652d20616e6420676f7665726e616e63652d626f756e64656429c820202d20605031602069732074686520636f6d706c6578697479206f66206070726f706f73616c6020707265696d6167652ea420202d20605032602069732070726f706f73616c2d636f756e742028636f64652d626f756e64656429040d01436f6e7461696e7320612076617269616e742070657220646973706174636861626c652065787472696e736963207468617420746869732070616c6c6574206861732e2d030c4470616c6c65745f6d656d626572736869701870616c6c65741043616c6c08045400044900011c286164645f6d656d62657204010c77686f550201504163636f756e7449644c6f6f6b75704f663c543e00000c784164642061206d656d626572206077686f6020746f20746865207365742e009c4d6179206f6e6c792062652063616c6c65642066726f6d2060543a3a4164644f726967696e602e3472656d6f76655f6d656d62657204010c77686f550201504163636f756e7449644c6f6f6b75704f663c543e00010c8c52656d6f76652061206d656d626572206077686f602066726f6d20746865207365742e00a84d6179206f6e6c792062652063616c6c65642066726f6d2060543a3a52656d6f76654f726967696e602e2c737761705f6d656d62657208011872656d6f7665550201504163636f756e7449644c6f6f6b75704f663c543e00010c616464550201504163636f756e7449644c6f6f6b75704f663c543e000214bc53776170206f7574206f6e65206d656d626572206072656d6f76656020666f7220616e6f746865722060616464602e00a04d6179206f6e6c792062652063616c6c65642066726f6d2060543a3a537761704f726967696e602e000d015072696d65206d656d62657273686970206973202a6e6f742a207061737365642066726f6d206072656d6f76656020746f2060616464602c20696620657874616e742e3472657365745f6d656d6265727304011c6d656d626572735d0201445665633c543a3a4163636f756e7449643e00031055014368616e676520746865206d656d6265727368697020746f2061206e6577207365742c20646973726567617264696e6720746865206578697374696e67206d656d626572736869702e204265206e69636520616e64687061737320606d656d6265727360207072652d736f727465642e00a44d6179206f6e6c792062652063616c6c65642066726f6d2060543a3a52657365744f726967696e602e286368616e67655f6b657904010c6e6577550201504163636f756e7449644c6f6f6b75704f663c543e000414d453776170206f7574207468652073656e64696e67206d656d62657220666f7220736f6d65206f74686572206b657920606e6577602e00f04d6179206f6e6c792062652063616c6c65642066726f6d20605369676e656460206f726967696e206f6620612063757272656e74206d656d6265722e001d015072696d65206d656d62657273686970206973207061737365642066726f6d20746865206f726967696e206163636f756e7420746f20606e6577602c20696620657874616e742e247365745f7072696d6504010c77686f550201504163636f756e7449644c6f6f6b75704f663c543e00050cbc53657420746865207072696d65206d656d6265722e204d75737420626520612063757272656e74206d656d6265722e00a44d6179206f6e6c792062652063616c6c65642066726f6d2060543a3a5072696d654f726967696e602e2c636c6561725f7072696d6500060c9452656d6f766520746865207072696d65206d656d626572206966206974206578697374732e00a44d6179206f6e6c792062652063616c6c65642066726f6d2060543a3a5072696d654f726967696e602e040d01436f6e7461696e7320612076617269616e742070657220646973706174636861626c652065787472696e736963207468617420746869732070616c6c6574206861732e31030c4470616c6c65745f6d656d626572736869701870616c6c65741043616c6c08045400044900011c286164645f6d656d62657204010c77686f550201504163636f756e7449644c6f6f6b75704f663c543e00000c784164642061206d656d626572206077686f6020746f20746865207365742e009c4d6179206f6e6c792062652063616c6c65642066726f6d2060543a3a4164644f726967696e602e3472656d6f76655f6d656d62657204010c77686f550201504163636f756e7449644c6f6f6b75704f663c543e00010c8c52656d6f76652061206d656d626572206077686f602066726f6d20746865207365742e00a84d6179206f6e6c792062652063616c6c65642066726f6d2060543a3a52656d6f76654f726967696e602e2c737761705f6d656d62657208011872656d6f7665550201504163636f756e7449644c6f6f6b75704f663c543e00010c616464550201504163636f756e7449644c6f6f6b75704f663c543e000214bc53776170206f7574206f6e65206d656d626572206072656d6f76656020666f7220616e6f746865722060616464602e00a04d6179206f6e6c792062652063616c6c65642066726f6d2060543a3a537761704f726967696e602e000d015072696d65206d656d62657273686970206973202a6e6f742a207061737365642066726f6d206072656d6f76656020746f2060616464602c20696620657874616e742e3472657365745f6d656d6265727304011c6d656d626572735d0201445665633c543a3a4163636f756e7449643e00031055014368616e676520746865206d656d6265727368697020746f2061206e6577207365742c20646973726567617264696e6720746865206578697374696e67206d656d626572736869702e204265206e69636520616e64687061737320606d656d6265727360207072652d736f727465642e00a44d6179206f6e6c792062652063616c6c65642066726f6d2060543a3a52657365744f726967696e602e286368616e67655f6b657904010c6e6577550201504163636f756e7449644c6f6f6b75704f663c543e000414d453776170206f7574207468652073656e64696e67206d656d62657220666f7220736f6d65206f74686572206b657920606e6577602e00f04d6179206f6e6c792062652063616c6c65642066726f6d20605369676e656460206f726967696e206f6620612063757272656e74206d656d6265722e001d015072696d65206d656d62657273686970206973207061737365642066726f6d20746865206f726967696e206163636f756e7420746f20606e6577602c20696620657874616e742e247365745f7072696d6504010c77686f550201504163636f756e7449644c6f6f6b75704f663c543e00050cbc53657420746865207072696d65206d656d6265722e204d75737420626520612063757272656e74206d656d6265722e00a44d6179206f6e6c792062652063616c6c65642066726f6d2060543a3a5072696d654f726967696e602e2c636c6561725f7072696d6500060c9452656d6f766520746865207072696d65206d656d626572206966206974206578697374732e00a44d6179206f6e6c792062652063616c6c65642066726f6d2060543a3a5072696d654f726967696e602e040d01436f6e7461696e7320612076617269616e742070657220646973706174636861626c652065787472696e736963207468617420746869732070616c6c6574206861732e35030c3870616c6c65745f7574696c6974791870616c6c65741043616c6c04045400011814626174636804011463616c6c733903017c5665633c3c5420617320436f6e6669673e3a3a52756e74696d6543616c6c3e0000487c53656e642061206261746368206f662064697370617463682063616c6c732e00b04d61792062652063616c6c65642066726f6d20616e79206f726967696e2065786365707420604e6f6e65602e005d012d206063616c6c73603a205468652063616c6c7320746f20626520646973706174636865642066726f6d207468652073616d65206f726967696e2e20546865206e756d626572206f662063616c6c206d757374206e6f74390120206578636565642074686520636f6e7374616e743a2060626174636865645f63616c6c735f6c696d6974602028617661696c61626c6520696e20636f6e7374616e74206d65746164617461292e0055014966206f726967696e20697320726f6f74207468656e207468652063616c6c7320617265206469737061746368656420776974686f757420636865636b696e67206f726967696e2066696c7465722e202854686973ec696e636c7564657320627970617373696e6720606672616d655f73797374656d3a3a436f6e6669673a3a4261736543616c6c46696c74657260292e0034232320436f6d706c6578697479d02d204f284329207768657265204320697320746865206e756d626572206f662063616c6c7320746f20626520626174636865642e005501546869732077696c6c2072657475726e20604f6b6020696e20616c6c2063697263756d7374616e6365732e20546f2064657465726d696e65207468652073756363657373206f66207468652062617463682c20616e31016576656e74206973206465706f73697465642e20496620612063616c6c206661696c656420616e64207468652062617463682077617320696e7465727275707465642c207468656e207468655501604261746368496e74657272757074656460206576656e74206973206465706f73697465642c20616c6f6e67207769746820746865206e756d626572206f66207375636365737366756c2063616c6c73206d6164654d01616e6420746865206572726f72206f6620746865206661696c65642063616c6c2e20496620616c6c2077657265207375636365737366756c2c207468656e2074686520604261746368436f6d706c65746564604c6576656e74206973206465706f73697465642e3461735f64657269766174697665080114696e6465789c010c75313600011063616c6c2503017c426f783c3c5420617320436f6e6669673e3a3a52756e74696d6543616c6c3e000134dc53656e6420612063616c6c207468726f75676820616e20696e64657865642070736575646f6e796d206f66207468652073656e6465722e00550146696c7465722066726f6d206f726967696e206172652070617373656420616c6f6e672e205468652063616c6c2077696c6c2062652064697370617463686564207769746820616e206f726967696e207768696368bc757365207468652073616d652066696c74657220617320746865206f726967696e206f6620746869732063616c6c2e0045014e4f54453a20496620796f75206e65656420746f20656e73757265207468617420616e79206163636f756e742d62617365642066696c746572696e67206973206e6f7420686f6e6f7265642028692e652e61016265636175736520796f7520657870656374206070726f78796020746f2068617665206265656e2075736564207072696f7220696e207468652063616c6c20737461636b20616e6420796f7520646f206e6f742077616e7451017468652063616c6c207265737472696374696f6e7320746f206170706c7920746f20616e79207375622d6163636f756e7473292c207468656e20757365206061735f6d756c74695f7468726573686f6c645f31607c696e20746865204d756c74697369672070616c6c657420696e73746561642e00f44e4f54453a205072696f7220746f2076657273696f6e202a31322c2074686973207761732063616c6c6564206061735f6c696d697465645f737562602e00cc546865206469737061746368206f726967696e20666f7220746869732063616c6c206d757374206265205f5369676e65645f2e2462617463685f616c6c04011463616c6c733903017c5665633c3c5420617320436f6e6669673e3a3a52756e74696d6543616c6c3e000234ec53656e642061206261746368206f662064697370617463682063616c6c7320616e642061746f6d6963616c6c792065786563757465207468656d2e21015468652077686f6c65207472616e73616374696f6e2077696c6c20726f6c6c6261636b20616e64206661696c20696620616e79206f66207468652063616c6c73206661696c65642e00b04d61792062652063616c6c65642066726f6d20616e79206f726967696e2065786365707420604e6f6e65602e005d012d206063616c6c73603a205468652063616c6c7320746f20626520646973706174636865642066726f6d207468652073616d65206f726967696e2e20546865206e756d626572206f662063616c6c206d757374206e6f74390120206578636565642074686520636f6e7374616e743a2060626174636865645f63616c6c735f6c696d6974602028617661696c61626c6520696e20636f6e7374616e74206d65746164617461292e0055014966206f726967696e20697320726f6f74207468656e207468652063616c6c7320617265206469737061746368656420776974686f757420636865636b696e67206f726967696e2066696c7465722e202854686973ec696e636c7564657320627970617373696e6720606672616d655f73797374656d3a3a436f6e6669673a3a4261736543616c6c46696c74657260292e0034232320436f6d706c6578697479d02d204f284329207768657265204320697320746865206e756d626572206f662063616c6c7320746f20626520626174636865642e2c64697370617463685f617308012461735f6f726967696e3d030154426f783c543a3a50616c6c6574734f726967696e3e00011063616c6c2503017c426f783c3c5420617320436f6e6669673e3a3a52756e74696d6543616c6c3e000318c84469737061746368657320612066756e6374696f6e2063616c6c207769746820612070726f7669646564206f726967696e2e00c4546865206469737061746368206f726967696e20666f7220746869732063616c6c206d757374206265205f526f6f745f2e0034232320436f6d706c65786974791c2d204f2831292e2c666f7263655f626174636804011463616c6c733903017c5665633c3c5420617320436f6e6669673e3a3a52756e74696d6543616c6c3e0004347c53656e642061206261746368206f662064697370617463682063616c6c732ed4556e6c696b6520606261746368602c20697420616c6c6f7773206572726f727320616e6420776f6e277420696e746572727570742e00b04d61792062652063616c6c65642066726f6d20616e79206f726967696e2065786365707420604e6f6e65602e005d012d206063616c6c73603a205468652063616c6c7320746f20626520646973706174636865642066726f6d207468652073616d65206f726967696e2e20546865206e756d626572206f662063616c6c206d757374206e6f74390120206578636565642074686520636f6e7374616e743a2060626174636865645f63616c6c735f6c696d6974602028617661696c61626c6520696e20636f6e7374616e74206d65746164617461292e004d014966206f726967696e20697320726f6f74207468656e207468652063616c6c732061726520646973706174636820776974686f757420636865636b696e67206f726967696e2066696c7465722e202854686973ec696e636c7564657320627970617373696e6720606672616d655f73797374656d3a3a436f6e6669673a3a4261736543616c6c46696c74657260292e0034232320436f6d706c6578697479d02d204f284329207768657265204320697320746865206e756d626572206f662063616c6c7320746f20626520626174636865642e2c776974685f77656967687408011063616c6c2503017c426f783c3c5420617320436f6e6669673e3a3a52756e74696d6543616c6c3e0001187765696768742c0118576569676874000518c4446973706174636820612066756e6374696f6e2063616c6c2077697468206120737065636966696564207765696768742e002d01546869732066756e6374696f6e20646f6573206e6f7420636865636b2074686520776569676874206f66207468652063616c6c2c20616e6420696e737465616420616c6c6f777320746865b8526f6f74206f726967696e20746f20737065636966792074686520776569676874206f66207468652063616c6c2e00c4546865206469737061746368206f726967696e20666f7220746869732063616c6c206d757374206265205f526f6f745f2e040d01436f6e7461696e7320612076617269616e742070657220646973706174636861626c652065787472696e736963207468617420746869732070616c6c6574206861732e39030000022503003d0308586e6f64655f73756274656e736f725f72756e74696d65304f726967696e43616c6c65720001101873797374656d0400410301746672616d655f73797374656d3a3a4f726967696e3c52756e74696d653e0000002c547269756d7669726174650400450301010170616c6c65745f636f6c6c6563746976653a3a4f726967696e3c52756e74696d652c2070616c6c65745f636f6c6c6563746976653a3a496e7374616e6365313e00080020457468657265756d04004903015c70616c6c65745f657468657265756d3a3a4f726967696e00150010566f69640400010201410173656c663a3a73705f6170695f68696464656e5f696e636c756465735f636f6e7374727563745f72756e74696d653a3a68696464656e5f696e636c7564653a3a0a5f5f707269766174653a3a566f69640003000041030c346672616d655f737570706f7274206469737061746368245261774f726967696e04244163636f756e7449640100010c10526f6f74000000185369676e656404000001244163636f756e744964000100104e6f6e65000200004503084470616c6c65745f636f6c6c656374697665245261774f726967696e08244163636f756e7449640100044900010c1c4d656d62657273080010012c4d656d626572436f756e74000010012c4d656d626572436f756e74000000184d656d62657204000001244163636f756e744964000100205f5068616e746f6d000200004903083c70616c6c65745f657468657265756d245261774f726967696e0001044c457468657265756d5472616e73616374696f6e04000d01011048313630000000004d030c2c70616c6c65745f7375646f1870616c6c65741043616c6c040454000114107375646f04011063616c6c2503017c426f783c3c5420617320436f6e6669673e3a3a52756e74696d6543616c6c3e000004350141757468656e7469636174657320746865207375646f206b657920616e64206469737061746368657320612066756e6374696f6e2063616c6c20776974682060526f6f7460206f726967696e2e547375646f5f756e636865636b65645f77656967687408011063616c6c2503017c426f783c3c5420617320436f6e6669673e3a3a52756e74696d6543616c6c3e0001187765696768742c0118576569676874000114350141757468656e7469636174657320746865207375646f206b657920616e64206469737061746368657320612066756e6374696f6e2063616c6c20776974682060526f6f7460206f726967696e2e2d01546869732066756e6374696f6e20646f6573206e6f7420636865636b2074686520776569676874206f66207468652063616c6c2c20616e6420696e737465616420616c6c6f777320746865b05375646f207573657220746f20737065636966792074686520776569676874206f66207468652063616c6c2e00cc546865206469737061746368206f726967696e20666f7220746869732063616c6c206d757374206265205f5369676e65645f2e1c7365745f6b657904010c6e6577550201504163636f756e7449644c6f6f6b75704f663c543e0002085d0141757468656e74696361746573207468652063757272656e74207375646f206b657920616e6420736574732074686520676976656e204163636f756e7449642028606e6577602920617320746865206e6577207375646f106b65792e1c7375646f5f617308010c77686f550201504163636f756e7449644c6f6f6b75704f663c543e00011063616c6c2503017c426f783c3c5420617320436f6e6669673e3a3a52756e74696d6543616c6c3e0003104d0141757468656e7469636174657320746865207375646f206b657920616e64206469737061746368657320612066756e6374696f6e2063616c6c207769746820605369676e656460206f726967696e2066726f6d406120676976656e206163636f756e742e00cc546865206469737061746368206f726967696e20666f7220746869732063616c6c206d757374206265205f5369676e65645f2e2872656d6f76655f6b657900040c845065726d616e656e746c792072656d6f76657320746865207375646f206b65792e006c2a2a546869732063616e6e6f7420626520756e2d646f6e652e2a2a040d01436f6e7461696e7320612076617269616e742070657220646973706174636861626c652065787472696e736963207468617420746869732070616c6c6574206861732e51030c3c70616c6c65745f6d756c74697369671870616c6c65741043616c6c0404540001105061735f6d756c74695f7468726573686f6c645f310801446f746865725f7369676e61746f726965735d0201445665633c543a3a4163636f756e7449643e00011063616c6c2503017c426f783c3c5420617320436f6e6669673e3a3a52756e74696d6543616c6c3e0000305101496d6d6564696174656c792064697370617463682061206d756c74692d7369676e61747572652063616c6c207573696e6720612073696e676c6520617070726f76616c2066726f6d207468652063616c6c65722e00cc546865206469737061746368206f726967696e20666f7220746869732063616c6c206d757374206265205f5369676e65645f2e003d012d20606f746865725f7369676e61746f72696573603a20546865206163636f756e747320286f74686572207468616e207468652073656e646572292077686f206172652070617274206f662074686501016d756c74692d7369676e61747572652c2062757420646f206e6f7420706172746963697061746520696e2074686520617070726f76616c2070726f636573732e882d206063616c6c603a205468652063616c6c20746f2062652065786563757465642e00b8526573756c74206973206571756976616c656e7420746f20746865206469737061746368656420726573756c742e0034232320436f6d706c657869747919014f285a202b204329207768657265205a20697320746865206c656e677468206f66207468652063616c6c20616e6420432069747320657865637574696f6e207765696768742e2061735f6d756c74691401247468726573686f6c649c010c7531360001446f746865725f7369676e61746f726965735d0201445665633c543a3a4163636f756e7449643e00013c6d617962655f74696d65706f696e74550301904f7074696f6e3c54696d65706f696e743c426c6f636b4e756d626572466f723c543e3e3e00011063616c6c2503017c426f783c3c5420617320436f6e6669673e3a3a52756e74696d6543616c6c3e0001286d61785f7765696768742c011857656967687400019c5501526567697374657220617070726f76616c20666f72206120646973706174636820746f206265206d6164652066726f6d20612064657465726d696e697374696320636f6d706f73697465206163636f756e74206966f8617070726f766564206279206120746f74616c206f6620607468726573686f6c64202d203160206f6620606f746865725f7369676e61746f72696573602e00b049662074686572652061726520656e6f7567682c207468656e206469737061746368207468652063616c6c2e002d015061796d656e743a20604465706f73697442617365602077696c6c20626520726573657276656420696620746869732069732074686520666972737420617070726f76616c2c20706c75733d01607468726573686f6c64602074696d657320604465706f736974466163746f72602e2049742069732072657475726e6564206f6e636520746869732064697370617463682068617070656e73206f723469732063616e63656c6c65642e00cc546865206469737061746368206f726967696e20666f7220746869732063616c6c206d757374206265205f5369676e65645f2e0055012d20607468726573686f6c64603a2054686520746f74616c206e756d626572206f6620617070726f76616c7320666f722074686973206469737061746368206265666f72652069742069732065786563757465642e41012d20606f746865725f7369676e61746f72696573603a20546865206163636f756e747320286f74686572207468616e207468652073656e646572292077686f2063616e20617070726f766520746869736c64697370617463682e204d6179206e6f7420626520656d7074792e59012d20606d617962655f74696d65706f696e74603a20496620746869732069732074686520666972737420617070726f76616c2c207468656e2074686973206d75737420626520604e6f6e65602e20496620697420697351016e6f742074686520666972737420617070726f76616c2c207468656e206974206d7573742062652060536f6d65602c2077697468207468652074696d65706f696e742028626c6f636b206e756d62657220616e64d47472616e73616374696f6e20696e64657829206f662074686520666972737420617070726f76616c207472616e73616374696f6e2e882d206063616c6c603a205468652063616c6c20746f2062652065786563757465642e001d014e4f54453a20556e6c6573732074686973206973207468652066696e616c20617070726f76616c2c20796f752077696c6c2067656e6572616c6c792077616e7420746f20757365190160617070726f76655f61735f6d756c74696020696e73746561642c2073696e6365206974206f6e6c7920726571756972657320612068617368206f66207468652063616c6c2e005901526573756c74206973206571756976616c656e7420746f20746865206469737061746368656420726573756c7420696620607468726573686f6c64602069732065786163746c79206031602e204f746865727769736555016f6e20737563636573732c20726573756c7420697320604f6b6020616e642074686520726573756c742066726f6d2074686520696e746572696f722063616c6c2c206966206974207761732065786563757465642cdc6d617920626520666f756e6420696e20746865206465706f736974656420604d756c7469736967457865637574656460206576656e742e0034232320436f6d706c6578697479502d20604f2853202b205a202b2043616c6c29602ecc2d20557020746f206f6e652062616c616e63652d72657365727665206f7220756e72657365727665206f7065726174696f6e2e3d012d204f6e6520706173737468726f756768206f7065726174696f6e2c206f6e6520696e736572742c20626f746820604f285329602077686572652060536020697320746865206e756d626572206f66450120207369676e61746f726965732e206053602069732063617070656420627920604d61785369676e61746f72696573602c207769746820776569676874206265696e672070726f706f7274696f6e616c2e21012d204f6e652063616c6c20656e636f6465202620686173682c20626f7468206f6620636f6d706c657869747920604f285a296020776865726520605a602069732074782d6c656e2ebc2d204f6e6520656e636f6465202620686173682c20626f7468206f6620636f6d706c657869747920604f285329602ed42d20557020746f206f6e652062696e6172792073656172636820616e6420696e736572742028604f286c6f6753202b20532960292ef82d20492f4f3a2031207265616420604f285329602c20757020746f2031206d757461746520604f285329602e20557020746f206f6e652072656d6f76652e302d204f6e65206576656e742e6c2d2054686520776569676874206f6620746865206063616c6c602e4d012d2053746f726167653a20696e7365727473206f6e65206974656d2c2076616c75652073697a6520626f756e64656420627920604d61785369676e61746f72696573602c20776974682061206465706f7369741901202074616b656e20666f7220697473206c69666574696d65206f6620604465706f73697442617365202b207468726573686f6c64202a204465706f736974466163746f72602e40617070726f76655f61735f6d756c74691401247468726573686f6c649c010c7531360001446f746865725f7369676e61746f726965735d0201445665633c543a3a4163636f756e7449643e00013c6d617962655f74696d65706f696e74550301904f7074696f6e3c54696d65706f696e743c426c6f636b4e756d626572466f723c543e3e3e00012463616c6c5f686173680401205b75383b2033325d0001286d61785f7765696768742c01185765696768740002785501526567697374657220617070726f76616c20666f72206120646973706174636820746f206265206d6164652066726f6d20612064657465726d696e697374696320636f6d706f73697465206163636f756e74206966f8617070726f766564206279206120746f74616c206f6620607468726573686f6c64202d203160206f6620606f746865725f7369676e61746f72696573602e002d015061796d656e743a20604465706f73697442617365602077696c6c20626520726573657276656420696620746869732069732074686520666972737420617070726f76616c2c20706c75733d01607468726573686f6c64602074696d657320604465706f736974466163746f72602e2049742069732072657475726e6564206f6e636520746869732064697370617463682068617070656e73206f723469732063616e63656c6c65642e00cc546865206469737061746368206f726967696e20666f7220746869732063616c6c206d757374206265205f5369676e65645f2e0055012d20607468726573686f6c64603a2054686520746f74616c206e756d626572206f6620617070726f76616c7320666f722074686973206469737061746368206265666f72652069742069732065786563757465642e41012d20606f746865725f7369676e61746f72696573603a20546865206163636f756e747320286f74686572207468616e207468652073656e646572292077686f2063616e20617070726f766520746869736c64697370617463682e204d6179206e6f7420626520656d7074792e59012d20606d617962655f74696d65706f696e74603a20496620746869732069732074686520666972737420617070726f76616c2c207468656e2074686973206d75737420626520604e6f6e65602e20496620697420697351016e6f742074686520666972737420617070726f76616c2c207468656e206974206d7573742062652060536f6d65602c2077697468207468652074696d65706f696e742028626c6f636b206e756d62657220616e64d47472616e73616374696f6e20696e64657829206f662074686520666972737420617070726f76616c207472616e73616374696f6e2ecc2d206063616c6c5f68617368603a205468652068617368206f66207468652063616c6c20746f2062652065786563757465642e0035014e4f54453a2049662074686973206973207468652066696e616c20617070726f76616c2c20796f752077696c6c2077616e7420746f20757365206061735f6d756c74696020696e73746561642e0034232320436f6d706c6578697479242d20604f285329602ecc2d20557020746f206f6e652062616c616e63652d72657365727665206f7220756e72657365727665206f7065726174696f6e2e3d012d204f6e6520706173737468726f756768206f7065726174696f6e2c206f6e6520696e736572742c20626f746820604f285329602077686572652060536020697320746865206e756d626572206f66450120207369676e61746f726965732e206053602069732063617070656420627920604d61785369676e61746f72696573602c207769746820776569676874206265696e672070726f706f7274696f6e616c2ebc2d204f6e6520656e636f6465202620686173682c20626f7468206f6620636f6d706c657869747920604f285329602ed42d20557020746f206f6e652062696e6172792073656172636820616e6420696e736572742028604f286c6f6753202b20532960292ef82d20492f4f3a2031207265616420604f285329602c20757020746f2031206d757461746520604f285329602e20557020746f206f6e652072656d6f76652e302d204f6e65206576656e742e4d012d2053746f726167653a20696e7365727473206f6e65206974656d2c2076616c75652073697a6520626f756e64656420627920604d61785369676e61746f72696573602c20776974682061206465706f7369741901202074616b656e20666f7220697473206c69666574696d65206f6620604465706f73697442617365202b207468726573686f6c64202a204465706f736974466163746f72602e3c63616e63656c5f61735f6d756c74691001247468726573686f6c649c010c7531360001446f746865725f7369676e61746f726965735d0201445665633c543a3a4163636f756e7449643e00012474696d65706f696e74d8017054696d65706f696e743c426c6f636b4e756d626572466f723c543e3e00012463616c6c5f686173680401205b75383b2033325d000354550143616e63656c2061207072652d6578697374696e672c206f6e2d676f696e67206d756c7469736967207472616e73616374696f6e2e20416e79206465706f7369742072657365727665642070726576696f75736c79c4666f722074686973206f7065726174696f6e2077696c6c20626520756e7265736572766564206f6e20737563636573732e00cc546865206469737061746368206f726967696e20666f7220746869732063616c6c206d757374206265205f5369676e65645f2e0055012d20607468726573686f6c64603a2054686520746f74616c206e756d626572206f6620617070726f76616c7320666f722074686973206469737061746368206265666f72652069742069732065786563757465642e41012d20606f746865725f7369676e61746f72696573603a20546865206163636f756e747320286f74686572207468616e207468652073656e646572292077686f2063616e20617070726f766520746869736c64697370617463682e204d6179206e6f7420626520656d7074792e5d012d206074696d65706f696e74603a205468652074696d65706f696e742028626c6f636b206e756d62657220616e64207472616e73616374696f6e20696e64657829206f662074686520666972737420617070726f76616c787472616e73616374696f6e20666f7220746869732064697370617463682ecc2d206063616c6c5f68617368603a205468652068617368206f66207468652063616c6c20746f2062652065786563757465642e0034232320436f6d706c6578697479242d20604f285329602ecc2d20557020746f206f6e652062616c616e63652d72657365727665206f7220756e72657365727665206f7065726174696f6e2e3d012d204f6e6520706173737468726f756768206f7065726174696f6e2c206f6e6520696e736572742c20626f746820604f285329602077686572652060536020697320746865206e756d626572206f66450120207369676e61746f726965732e206053602069732063617070656420627920604d61785369676e61746f72696573602c207769746820776569676874206265696e672070726f706f7274696f6e616c2ebc2d204f6e6520656e636f6465202620686173682c20626f7468206f6620636f6d706c657869747920604f285329602e302d204f6e65206576656e742e842d20492f4f3a2031207265616420604f285329602c206f6e652072656d6f76652e702d2053746f726167653a2072656d6f766573206f6e65206974656d2e040d01436f6e7461696e7320612076617269616e742070657220646973706174636861626c652065787472696e736963207468617420746869732070616c6c6574206861732e550304184f7074696f6e04045401d80108104e6f6e6500000010536f6d650400d8000001000059030c3c70616c6c65745f707265696d6167651870616c6c65741043616c6c040454000114346e6f74655f707265696d616765040114627974657338011c5665633c75383e000010745265676973746572206120707265696d616765206f6e2d636861696e2e00550149662074686520707265696d616765207761732070726576696f75736c79207265717565737465642c206e6f2066656573206f72206465706f73697473206172652074616b656e20666f722070726f766964696e67550174686520707265696d6167652e204f74686572776973652c2061206465706f7369742069732074616b656e2070726f706f7274696f6e616c20746f207468652073697a65206f662074686520707265696d6167652e3c756e6e6f74655f707265696d6167650401106861736834011c543a3a48617368000118dc436c65617220616e20756e72657175657374656420707265696d6167652066726f6d207468652072756e74696d652073746f726167652e00fc496620606c656e602069732070726f76696465642c207468656e2069742077696c6c2062652061206d7563682063686561706572206f7065726174696f6e2e0001012d206068617368603a205468652068617368206f662074686520707265696d61676520746f2062652072656d6f7665642066726f6d207468652073746f72652eb82d20606c656e603a20546865206c656e677468206f662074686520707265696d616765206f66206068617368602e40726571756573745f707265696d6167650401106861736834011c543a3a48617368000210410152657175657374206120707265696d6167652062652075706c6f6164656420746f2074686520636861696e20776974686f757420706179696e6720616e792066656573206f72206465706f736974732e00550149662074686520707265696d6167652072657175657374732068617320616c7265616479206265656e2070726f7669646564206f6e2d636861696e2c20776520756e7265736572766520616e79206465706f7369743901612075736572206d6179206861766520706169642c20616e642074616b652074686520636f6e74726f6c206f662074686520707265696d616765206f7574206f662074686569722068616e64732e48756e726571756573745f707265696d6167650401106861736834011c543a3a4861736800030cbc436c65617220612070726576696f75736c79206d616465207265717565737420666f72206120707265696d6167652e002d014e4f54453a2054484953204d555354204e4f542042452043414c4c4544204f4e20606861736860204d4f52452054494d4553205448414e2060726571756573745f707265696d616765602e38656e737572655f75706461746564040118686173686573b401305665633c543a3a486173683e00040cc4456e7375726520746861742074686520612062756c6b206f66207072652d696d616765732069732075706772616465642e003d015468652063616c6c65722070617973206e6f20666565206966206174206c6561737420393025206f66207072652d696d616765732077657265207375636365737366756c6c7920757064617465642e040d01436f6e7461696e7320612076617269616e742070657220646973706174636861626c652065787472696e736963207468617420746869732070616c6c6574206861732e5d030c4070616c6c65745f7363686564756c65721870616c6c65741043616c6c040454000128207363686564756c651001107768656e100144426c6f636b4e756d626572466f723c543e0001386d617962655f706572696f646963610301ac4f7074696f6e3c7363686564756c653a3a506572696f643c426c6f636b4e756d626572466f723c543e3e3e0001207072696f726974790801487363686564756c653a3a5072696f7269747900011063616c6c2503017c426f783c3c5420617320436f6e6669673e3a3a52756e74696d6543616c6c3e00000470416e6f6e796d6f75736c79207363686564756c652061207461736b2e1863616e63656c0801107768656e100144426c6f636b4e756d626572466f723c543e000114696e64657810010c7533320001049443616e63656c20616e20616e6f6e796d6f75736c79207363686564756c6564207461736b2e387363686564756c655f6e616d656414010869640401205461736b4e616d650001107768656e100144426c6f636b4e756d626572466f723c543e0001386d617962655f706572696f646963610301ac4f7074696f6e3c7363686564756c653a3a506572696f643c426c6f636b4e756d626572466f723c543e3e3e0001207072696f726974790801487363686564756c653a3a5072696f7269747900011063616c6c2503017c426f783c3c5420617320436f6e6669673e3a3a52756e74696d6543616c6c3e000204585363686564756c652061206e616d6564207461736b2e3063616e63656c5f6e616d656404010869640401205461736b4e616d650003047843616e63656c2061206e616d6564207363686564756c6564207461736b2e387363686564756c655f61667465721001146166746572100144426c6f636b4e756d626572466f723c543e0001386d617962655f706572696f646963610301ac4f7074696f6e3c7363686564756c653a3a506572696f643c426c6f636b4e756d626572466f723c543e3e3e0001207072696f726974790801487363686564756c653a3a5072696f7269747900011063616c6c2503017c426f783c3c5420617320436f6e6669673e3a3a52756e74696d6543616c6c3e000404a8416e6f6e796d6f75736c79207363686564756c652061207461736b20616674657220612064656c61792e507363686564756c655f6e616d65645f616674657214010869640401205461736b4e616d650001146166746572100144426c6f636b4e756d626572466f723c543e0001386d617962655f706572696f646963610301ac4f7074696f6e3c7363686564756c653a3a506572696f643c426c6f636b4e756d626572466f723c543e3e3e0001207072696f726974790801487363686564756c653a3a5072696f7269747900011063616c6c2503017c426f783c3c5420617320436f6e6669673e3a3a52756e74696d6543616c6c3e000504905363686564756c652061206e616d6564207461736b20616674657220612064656c61792e247365745f72657472790c01107461736be401785461736b416464726573733c426c6f636b4e756d626572466f723c543e3e00011c726574726965730801087538000118706572696f64100144426c6f636b4e756d626572466f723c543e0006305901536574206120726574727920636f6e66696775726174696f6e20666f722061207461736b20736f20746861742c20696e206361736520697473207363686564756c65642072756e206661696c732c2069742077696c6c5501626520726574726965642061667465722060706572696f646020626c6f636b732c20666f72206120746f74616c20616d6f756e74206f66206072657472696573602072657472696573206f7220756e74696c2069742473756363656564732e0055015461736b73207768696368206e65656420746f206265207363686564756c656420666f72206120726574727920617265207374696c6c207375626a65637420746f20776569676874206d65746572696e6720616e6451016167656e64612073706163652c2073616d65206173206120726567756c6172207461736b2e204966206120706572696f646963207461736b206661696c732c2069742077696c6c206265207363686564756c6564906e6f726d616c6c79207768696c6520746865207461736b206973207265747279696e672e0051015461736b73207363686564756c6564206173206120726573756c74206f66206120726574727920666f72206120706572696f646963207461736b2061726520756e6e616d65642c206e6f6e2d706572696f6469633d01636c6f6e6573206f6620746865206f726967696e616c207461736b2e20546865697220726574727920636f6e66696775726174696f6e2077696c6c20626520646572697665642066726f6d207468654d016f726967696e616c207461736b277320636f6e66696775726174696f6e2c206275742077696c6c20686176652061206c6f7765722076616c756520666f72206072656d61696e696e6760207468616e20746865646f726967696e616c2060746f74616c5f72657472696573602e3c7365745f72657472795f6e616d65640c010869640401205461736b4e616d6500011c726574726965730801087538000118706572696f64100144426c6f636b4e756d626572466f723c543e0007305d01536574206120726574727920636f6e66696775726174696f6e20666f722061206e616d6564207461736b20736f20746861742c20696e206361736520697473207363686564756c65642072756e206661696c732c2069745d0177696c6c20626520726574726965642061667465722060706572696f646020626c6f636b732c20666f72206120746f74616c20616d6f756e74206f66206072657472696573602072657472696573206f7220756e74696c3069742073756363656564732e0055015461736b73207768696368206e65656420746f206265207363686564756c656420666f72206120726574727920617265207374696c6c207375626a65637420746f20776569676874206d65746572696e6720616e6451016167656e64612073706163652c2073616d65206173206120726567756c6172207461736b2e204966206120706572696f646963207461736b206661696c732c2069742077696c6c206265207363686564756c6564906e6f726d616c6c79207768696c6520746865207461736b206973207265747279696e672e0051015461736b73207363686564756c6564206173206120726573756c74206f66206120726574727920666f72206120706572696f646963207461736b2061726520756e6e616d65642c206e6f6e2d706572696f6469633d01636c6f6e6573206f6620746865206f726967696e616c207461736b2e20546865697220726574727920636f6e66696775726174696f6e2077696c6c20626520646572697665642066726f6d207468654d016f726967696e616c207461736b277320636f6e66696775726174696f6e2c206275742077696c6c20686176652061206c6f7765722076616c756520666f72206072656d61696e696e6760207468616e20746865646f726967696e616c2060746f74616c5f72657472696573602e3063616e63656c5f72657472790401107461736be401785461736b416464726573733c426c6f636b4e756d626572466f723c543e3e000804a852656d6f7665732074686520726574727920636f6e66696775726174696f6e206f662061207461736b2e4863616e63656c5f72657472795f6e616d656404010869640401205461736b4e616d65000904bc43616e63656c2074686520726574727920636f6e66696775726174696f6e206f662061206e616d6564207461736b2e040d01436f6e7461696e7320612076617269616e742070657220646973706174636861626c652065787472696e736963207468617420746869732070616c6c6574206861732e610304184f7074696f6e04045401e40108104e6f6e6500000010536f6d650400e4000001000065030c3070616c6c65745f70726f78791870616c6c65741043616c6c0404540001281470726f78790c01107265616c550201504163636f756e7449644c6f6f6b75704f663c543e000140666f7263655f70726f78795f74797065690301504f7074696f6e3c543a3a50726f7879547970653e00011063616c6c2503017c426f783c3c5420617320436f6e6669673e3a3a52756e74696d6543616c6c3e0000244d0144697370617463682074686520676976656e206063616c6c602066726f6d20616e206163636f756e742074686174207468652073656e64657220697320617574686f726973656420666f72207468726f75676830606164645f70726f7879602e00cc546865206469737061746368206f726967696e20666f7220746869732063616c6c206d757374206265205f5369676e65645f2e002c506172616d65746572733a0d012d20607265616c603a20546865206163636f756e742074686174207468652070726f78792077696c6c206d616b6520612063616c6c206f6e20626568616c66206f662e61012d2060666f7263655f70726f78795f74797065603a2053706563696679207468652065786163742070726f7879207479706520746f206265207573656420616e6420636865636b656420666f7220746869732063616c6c2ed02d206063616c6c603a205468652063616c6c20746f206265206d6164652062792074686520607265616c60206163636f756e742e246164645f70726f78790c012064656c6567617465550201504163636f756e7449644c6f6f6b75704f663c543e00012870726f78795f74797065f00130543a3a50726f78795479706500011464656c6179100144426c6f636b4e756d626572466f723c543e0001244501526567697374657220612070726f7879206163636f756e7420666f72207468652073656e64657220746861742069732061626c6520746f206d616b652063616c6c73206f6e2069747320626568616c662e00cc546865206469737061746368206f726967696e20666f7220746869732063616c6c206d757374206265205f5369676e65645f2e002c506172616d65746572733a11012d206070726f7879603a20546865206163636f756e74207468617420746865206063616c6c65726020776f756c64206c696b6520746f206d616b6520612070726f78792efc2d206070726f78795f74797065603a20546865207065726d697373696f6e7320616c6c6f77656420666f7220746869732070726f7879206163636f756e742e4d012d206064656c6179603a2054686520616e6e6f756e63656d656e7420706572696f64207265717569726564206f662074686520696e697469616c2070726f78792e2057696c6c2067656e6572616c6c79206265147a65726f2e3072656d6f76655f70726f78790c012064656c6567617465550201504163636f756e7449644c6f6f6b75704f663c543e00012870726f78795f74797065f00130543a3a50726f78795479706500011464656c6179100144426c6f636b4e756d626572466f723c543e00021ca8556e726567697374657220612070726f7879206163636f756e7420666f72207468652073656e6465722e00cc546865206469737061746368206f726967696e20666f7220746869732063616c6c206d757374206265205f5369676e65645f2e002c506172616d65746572733a25012d206070726f7879603a20546865206163636f756e74207468617420746865206063616c6c65726020776f756c64206c696b6520746f2072656d6f766520617320612070726f78792e41012d206070726f78795f74797065603a20546865207065726d697373696f6e732063757272656e746c7920656e61626c656420666f72207468652072656d6f7665642070726f7879206163636f756e742e3872656d6f76655f70726f78696573000318b4556e726567697374657220616c6c2070726f7879206163636f756e747320666f72207468652073656e6465722e00cc546865206469737061746368206f726967696e20666f7220746869732063616c6c206d757374206265205f5369676e65645f2e0041015741524e494e473a2054686973206d61792062652063616c6c6564206f6e206163636f756e74732063726561746564206279206070757265602c20686f776576657220696620646f6e652c207468656e590174686520756e726573657276656420666565732077696c6c20626520696e61636365737369626c652e202a2a416c6c2061636365737320746f2074686973206163636f756e742077696c6c206265206c6f73742e2a2a2c6372656174655f707572650c012870726f78795f74797065f00130543a3a50726f78795479706500011464656c6179100144426c6f636b4e756d626572466f723c543e000114696e6465789c010c7531360004483901537061776e2061206672657368206e6577206163636f756e7420746861742069732067756172616e7465656420746f206265206f746865727769736520696e61636365737369626c652c20616e64fc696e697469616c697a65206974207769746820612070726f7879206f66206070726f78795f747970656020666f7220606f726967696e602073656e6465722e006c5265717569726573206120605369676e656460206f726967696e2e0051012d206070726f78795f74797065603a205468652074797065206f66207468652070726f78792074686174207468652073656e6465722077696c6c2062652072656769737465726564206173206f766572207468654d016e6577206163636f756e742e20546869732077696c6c20616c6d6f737420616c7761797320626520746865206d6f7374207065726d697373697665206050726f7879547970656020706f737369626c6520746f78616c6c6f7720666f72206d6178696d756d20666c65786962696c6974792e51012d2060696e646578603a204120646973616d626967756174696f6e20696e6465782c20696e206361736520746869732069732063616c6c6564206d756c7469706c652074696d657320696e207468652073616d655d017472616e73616374696f6e2028652e672e207769746820607574696c6974793a3a626174636860292e20556e6c65737320796f75277265207573696e67206062617463686020796f752070726f6261626c79206a7573744077616e7420746f20757365206030602e4d012d206064656c6179603a2054686520616e6e6f756e63656d656e7420706572696f64207265717569726564206f662074686520696e697469616c2070726f78792e2057696c6c2067656e6572616c6c79206265147a65726f2e0051014661696c73207769746820604475706c69636174656020696620746869732068617320616c7265616479206265656e2063616c6c656420696e2074686973207472616e73616374696f6e2c2066726f6d207468659873616d652073656e6465722c2077697468207468652073616d6520706172616d65746572732e00e44661696c732069662074686572652061726520696e73756666696369656e742066756e647320746f2070617920666f72206465706f7369742e246b696c6c5f7075726514011c737061776e6572550201504163636f756e7449644c6f6f6b75704f663c543e00012870726f78795f74797065f00130543a3a50726f787954797065000114696e6465789c010c75313600011868656967687461010144426c6f636b4e756d626572466f723c543e0001246578745f696e6465786101010c753332000540a052656d6f76657320612070726576696f75736c7920737061776e656420707572652070726f78792e0049015741524e494e473a202a2a416c6c2061636365737320746f2074686973206163636f756e742077696c6c206265206c6f73742e2a2a20416e792066756e64732068656c6420696e2069742077696c6c20626534696e61636365737369626c652e0059015265717569726573206120605369676e656460206f726967696e2c20616e64207468652073656e646572206163636f756e74206d7573742068617665206265656e206372656174656420627920612063616c6c20746f94607075726560207769746820636f72726573706f6e64696e6720706172616d65746572732e0039012d2060737061776e6572603a20546865206163636f756e742074686174206f726967696e616c6c792063616c6c65642060707572656020746f206372656174652074686973206163636f756e742e39012d2060696e646578603a2054686520646973616d626967756174696f6e20696e646578206f726967696e616c6c792070617373656420746f206070757265602e2050726f6261626c79206030602eec2d206070726f78795f74797065603a205468652070726f78792074797065206f726967696e616c6c792070617373656420746f206070757265602e29012d2060686569676874603a2054686520686569676874206f662074686520636861696e207768656e207468652063616c6c20746f20607075726560207761732070726f6365737365642e35012d20606578745f696e646578603a205468652065787472696e73696320696e64657820696e207768696368207468652063616c6c20746f20607075726560207761732070726f6365737365642e0035014661696c73207769746820604e6f5065726d697373696f6e6020696e2063617365207468652063616c6c6572206973206e6f7420612070726576696f75736c7920637265617465642070757265dc6163636f756e742077686f7365206070757265602063616c6c2068617320636f72726573706f6e64696e6720706172616d65746572732e20616e6e6f756e63650801107265616c550201504163636f756e7449644c6f6f6b75704f663c543e00012463616c6c5f6861736834013443616c6c486173684f663c543e00063c05015075626c697368207468652068617368206f6620612070726f78792d63616c6c20746861742077696c6c206265206d61646520696e20746865206675747572652e005d0154686973206d7573742062652063616c6c656420736f6d65206e756d626572206f6620626c6f636b73206265666f72652074686520636f72726573706f6e64696e67206070726f78796020697320617474656d7074656425016966207468652064656c6179206173736f6369617465642077697468207468652070726f78792072656c6174696f6e736869702069732067726561746572207468616e207a65726f2e0011014e6f206d6f7265207468616e20604d617850656e64696e676020616e6e6f756e63656d656e7473206d6179206265206d61646520617420616e79206f6e652074696d652e000901546869732077696c6c2074616b652061206465706f736974206f662060416e6e6f756e63656d656e744465706f736974466163746f72602061732077656c6c206173190160416e6e6f756e63656d656e744465706f736974426173656020696620746865726520617265206e6f206f746865722070656e64696e6720616e6e6f756e63656d656e74732e002501546865206469737061746368206f726967696e20666f7220746869732063616c6c206d757374206265205f5369676e65645f20616e6420612070726f7879206f6620607265616c602e002c506172616d65746572733a0d012d20607265616c603a20546865206163636f756e742074686174207468652070726f78792077696c6c206d616b6520612063616c6c206f6e20626568616c66206f662e15012d206063616c6c5f68617368603a205468652068617368206f66207468652063616c6c20746f206265206d6164652062792074686520607265616c60206163636f756e742e4c72656d6f76655f616e6e6f756e63656d656e740801107265616c550201504163636f756e7449644c6f6f6b75704f663c543e00012463616c6c5f6861736834013443616c6c486173684f663c543e0007287052656d6f7665206120676976656e20616e6e6f756e63656d656e742e0059014d61792062652063616c6c656420627920612070726f7879206163636f756e7420746f2072656d6f766520612063616c6c20746865792070726576696f75736c7920616e6e6f756e63656420616e642072657475726e30746865206465706f7369742e00cc546865206469737061746368206f726967696e20666f7220746869732063616c6c206d757374206265205f5369676e65645f2e002c506172616d65746572733a0d012d20607265616c603a20546865206163636f756e742074686174207468652070726f78792077696c6c206d616b6520612063616c6c206f6e20626568616c66206f662e15012d206063616c6c5f68617368603a205468652068617368206f66207468652063616c6c20746f206265206d6164652062792074686520607265616c60206163636f756e742e4c72656a6563745f616e6e6f756e63656d656e7408012064656c6567617465550201504163636f756e7449644c6f6f6b75704f663c543e00012463616c6c5f6861736834013443616c6c486173684f663c543e000828b052656d6f76652074686520676976656e20616e6e6f756e63656d656e74206f6620612064656c65676174652e0061014d61792062652063616c6c6564206279206120746172676574202870726f7869656429206163636f756e7420746f2072656d6f766520612063616c6c2074686174206f6e65206f662074686569722064656c6567617465732501286064656c656761746560292068617320616e6e6f756e63656420746865792077616e7420746f20657865637574652e20546865206465706f7369742069732072657475726e65642e00cc546865206469737061746368206f726967696e20666f7220746869732063616c6c206d757374206265205f5369676e65645f2e002c506172616d65746572733af42d206064656c6567617465603a20546865206163636f756e7420746861742070726576696f75736c7920616e6e6f756e636564207468652063616c6c2ebc2d206063616c6c5f68617368603a205468652068617368206f66207468652063616c6c20746f206265206d6164652e3c70726f78795f616e6e6f756e63656410012064656c6567617465550201504163636f756e7449644c6f6f6b75704f663c543e0001107265616c550201504163636f756e7449644c6f6f6b75704f663c543e000140666f7263655f70726f78795f74797065690301504f7074696f6e3c543a3a50726f7879547970653e00011063616c6c2503017c426f783c3c5420617320436f6e6669673e3a3a52756e74696d6543616c6c3e00092c4d0144697370617463682074686520676976656e206063616c6c602066726f6d20616e206163636f756e742074686174207468652073656e64657220697320617574686f72697a656420666f72207468726f75676830606164645f70726f7879602e00a852656d6f76657320616e7920636f72726573706f6e64696e6720616e6e6f756e63656d656e742873292e00cc546865206469737061746368206f726967696e20666f7220746869732063616c6c206d757374206265205f5369676e65645f2e002c506172616d65746572733a0d012d20607265616c603a20546865206163636f756e742074686174207468652070726f78792077696c6c206d616b6520612063616c6c206f6e20626568616c66206f662e61012d2060666f7263655f70726f78795f74797065603a2053706563696679207468652065786163742070726f7879207479706520746f206265207573656420616e6420636865636b656420666f7220746869732063616c6c2ed02d206063616c6c603a205468652063616c6c20746f206265206d6164652062792074686520607265616c60206163636f756e742e040d01436f6e7461696e7320612076617269616e742070657220646973706174636861626c652065787472696e736963207468617420746869732070616c6c6574206861732e690304184f7074696f6e04045401f00108104e6f6e6500000010536f6d650400f000000100006d030c3c70616c6c65745f72656769737472791870616c6c65741043616c6c040454000108307365745f6964656e746974790801286964656e746966696564000130543a3a4163636f756e744964000110696e666f710301a4426f783c4964656e74697479496e666f3c543a3a4d61784164646974696f6e616c4669656c64733e3e0000043d01526567697374657220616e206964656e7469747920666f7220616e206163636f756e742e20546869732077696c6c206f766572777269746520616e79206578697374696e67206964656e746974792e38636c6561725f6964656e746974790401286964656e746966696564000130543a3a4163636f756e74496400010484436c65617220746865206964656e74697479206f6620616e206163636f756e742e040d01436f6e7461696e7320612076617269616e742070657220646973706174636861626c652065787472696e736963207468617420746869732070616c6c6574206861732e71030c3c70616c6c65745f7265676973747279147479706573304964656e74697479496e666f04284669656c644c696d697400002401286164646974696f6e616c75030190426f756e6465645665633c28446174612c2044617461292c204669656c644c696d69743e00011c646973706c61797d030110446174610001146c6567616c7d0301104461746100010c7765627d0301104461746100011072696f747d03011044617461000114656d61696c7d0301104461746100013c7067705f66696e6765727072696e74750401404f7074696f6e3c5b75383b2032305d3e000114696d6167657d0301104461746100011c747769747465727d03011044617461000075030c4c626f756e6465645f636f6c6c656374696f6e732c626f756e6465645f76656328426f756e646564566563080454017903045300000400710401185665633c543e00007903000004087d037d03007d030c3c70616c6c65745f7265676973747279147479706573104461746100011901104e6f6e65000000105261773004008103000001001052617731040085030000020010526177320400890300000300105261773304008d030000040010526177340400480000050010526177350400910300000600105261773604009503000007001052617737040099030000080010526177380400a50100000900105261773904009d0300000a001452617731300400a10300000b001452617731310400a50300000c001452617731320400a90300000d001452617731330400ad0300000e001452617731340400b10300000f001452617731350400b503000010001452617731360400b903000011001452617731370400bd03000012001452617731380400c103000013001452617731390400c5030000140014526177323004001101000015001452617732310400c903000016001452617732320400cd03000017001452617732330400d103000018001452617732340400d503000019001452617732350400d90300001a001452617732360400dd0300001b001452617732370400e10300001c001452617732380400e50300001d001452617732390400e90300001e001452617733300400ed0300001f001452617733310400f10300002000145261773332040004000021001452617733330400f503000022001452617733340400f903000023001452617733350400fd030000240014526177333604000104000025001452617733370400050400002600145261773338040009040000270014526177333904000d040000280014526177343004001104000029001452617734310400150400002a001452617734320400190400002b0014526177343304001d0400002c001452617734340400210400002d001452617734350400250400002e001452617734360400290400002f0014526177343704002d040000300014526177343804003104000031001452617734390400350400003200145261773530040039040000330014526177353104003d040000340014526177353204004104000035001452617735330400450400003600145261773534040049040000370014526177353504004d040000380014526177353604005104000039001452617735370400550400003a001452617735380400590400003b0014526177353904005d0400003c001452617736300400610400003d001452617736310400650400003e001452617736320400690400003f0014526177363304006d04000040001452617736340400ed01000041002c426c616b6554776f323536040004000042001853686132353604000400004300244b656363616b323536040004000044002c536861546872656532353604000400004500008103000003000000000800850300000301000000080089030000030200000008008d030000030300000008009103000003050000000800950300000306000000080099030000030700000008009d03000003090000000800a1030000030a0000000800a5030000030b0000000800a9030000030c0000000800ad030000030d0000000800b1030000030e0000000800b5030000030f0000000800b903000003100000000800bd03000003110000000800c103000003120000000800c503000003130000000800c903000003150000000800cd03000003160000000800d103000003170000000800d503000003180000000800d903000003190000000800dd030000031a0000000800e1030000031b0000000800e5030000031c0000000800e9030000031d0000000800ed030000031e0000000800f1030000031f0000000800f503000003210000000800f903000003220000000800fd030000032300000008000104000003240000000800050400000325000000080009040000032600000008000d040000032700000008001104000003280000000800150400000329000000080019040000032a00000008001d040000032b000000080021040000032c000000080025040000032d000000080029040000032e00000008002d040000032f00000008003104000003300000000800350400000331000000080039040000033200000008003d040000033300000008004104000003340000000800450400000335000000080049040000033600000008004d040000033700000008005104000003380000000800550400000339000000080059040000033a00000008005d040000033b000000080061040000033c000000080065040000033d000000080069040000033e00000008006d040000033f00000008007104000002790300750404184f7074696f6e0404540111010108104e6f6e6500000010536f6d6504001101000001000079040c4870616c6c65745f636f6d6d69746d656e74731870616c6c65741043616c6c040454000108387365745f636f6d6d69746d656e740801186e65747569649c010c753136000110696e666f7d040184426f783c436f6d6d69746d656e74496e666f3c543a3a4d61784669656c64733e3e000004945365742074686520636f6d6d69746d656e7420666f72206120676976656e206e6574756964387365745f726174655f6c696d6974040144726174655f6c696d69745f626c6f636b7310010c753332000104885375646f2d7365742074686520636f6d6d69746d656e742072617465206c696d6974040d01436f6e7461696e7320612076617269616e742070657220646973706174636861626c652065787472696e736963207468617420746869732070616c6c6574206861732e7d040c4870616c6c65745f636f6d6d69746d656e747314747970657338436f6d6d69746d656e74496e666f04284669656c644c696d697400000401186669656c647381040170426f756e6465645665633c446174612c204669656c644c696d69743e000081040c4c626f756e6465645f636f6c6c656374696f6e732c626f756e6465645f76656328426f756e646564566563080454018504045300000400890501185665633c543e000085040c4870616c6c65745f636f6d6d69746d656e7473147479706573104461746100011902104e6f6e65000000105261773004008103000001001052617731040085030000020010526177320400890300000300105261773304008d030000040010526177340400480000050010526177350400910300000600105261773604009503000007001052617737040099030000080010526177380400a50100000900105261773904009d0300000a001452617731300400a10300000b001452617731310400a50300000c001452617731320400a90300000d001452617731330400ad0300000e001452617731340400b10300000f001452617731350400b503000010001452617731360400b903000011001452617731370400bd03000012001452617731380400c103000013001452617731390400c5030000140014526177323004001101000015001452617732310400c903000016001452617732320400cd03000017001452617732330400d103000018001452617732340400d503000019001452617732350400d90300001a001452617732360400dd0300001b001452617732370400e10300001c001452617732380400e50300001d001452617732390400e90300001e001452617733300400ed0300001f001452617733310400f10300002000145261773332040004000021001452617733330400f503000022001452617733340400f903000023001452617733350400fd030000240014526177333604000104000025001452617733370400050400002600145261773338040009040000270014526177333904000d040000280014526177343004001104000029001452617734310400150400002a001452617734320400190400002b0014526177343304001d0400002c001452617734340400210400002d001452617734350400250400002e001452617734360400290400002f0014526177343704002d040000300014526177343804003104000031001452617734390400350400003200145261773530040039040000330014526177353104003d040000340014526177353204004104000035001452617735330400450400003600145261773534040049040000370014526177353504004d040000380014526177353604005104000039001452617735370400550400003a001452617735380400590400003b0014526177353904005d0400003c001452617736300400610400003d001452617736310400650400003e001452617736320400690400003f0014526177363304006d04000040001452617736340400ed0100004100145261773635040089040000420014526177363604008d040000430014526177363704009104000044001452617736380400950400004500145261773639040099040000460014526177373004009d04000047001452617737310400a104000048001452617737320400a504000049001452617737330400a90400004a001452617737340400ad0400004b001452617737350400b10400004c001452617737360400b50400004d001452617737370400b90400004e001452617737380400bd0400004f001452617737390400c104000050001452617738300400c504000051001452617738310400c904000052001452617738320400cd04000053001452617738330400d104000054001452617738340400d504000055001452617738350400d904000056001452617738360400dd04000057001452617738370400e104000058001452617738380400e504000059001452617738390400e90400005a001452617739300400ed0400005b001452617739310400f10400005c001452617739320400f50400005d001452617739330400f90400005e001452617739340400fd0400005f0014526177393504000105000060001452617739360400050500006100145261773937040009050000620014526177393804000d050000630014526177393904001105000064001852617731303004001505000065001852617731303104001905000066001852617731303204001d0500006700185261773130330400210500006800185261773130340400250500006900185261773130350400290500006a001852617731303604002d0500006b00185261773130370400310500006c00185261773130380400350500006d00185261773130390400390500006e001852617731313004003d0500006f001852617731313104004105000070001852617731313204004505000071001852617731313304004905000072001852617731313404004d05000073001852617731313504005105000074001852617731313604005505000075001852617731313704005905000076001852617731313804005d0500007700185261773131390400610500007800185261773132300400650500007900185261773132310400690500007a001852617731323204006d0500007b00185261773132330400710500007c00185261773132340400750500007d00185261773132350400790500007e001852617731323604007d0500007f001852617731323704008105000080001852617731323804008505000081002c426c616b6554776f323536040004000082001853686132353604000400008300244b656363616b323536040004000084002c5368615468726565323536040004000085000089040000034100000008008d040000034200000008009104000003430000000800950400000344000000080099040000034500000008009d04000003460000000800a104000003470000000800a504000003480000000800a904000003490000000800ad040000034a0000000800b1040000034b0000000800b5040000034c0000000800b9040000034d0000000800bd040000034e0000000800c1040000034f0000000800c504000003500000000800c904000003510000000800cd04000003520000000800d104000003530000000800d504000003540000000800d904000003550000000800dd04000003560000000800e104000003570000000800e504000003580000000800e904000003590000000800ed040000035a0000000800f1040000035b0000000800f5040000035c0000000800f9040000035d0000000800fd040000035e000000080001050000035f0000000800050500000360000000080009050000036100000008000d050000036200000008001105000003630000000800150500000364000000080019050000036500000008001d050000036600000008002105000003670000000800250500000368000000080029050000036900000008002d050000036a000000080031050000036b000000080035050000036c000000080039050000036d00000008003d050000036e000000080041050000036f0000000800450500000370000000080049050000037100000008004d050000037200000008005105000003730000000800550500000374000000080059050000037500000008005d050000037600000008006105000003770000000800650500000378000000080069050000037900000008006d050000037a000000080071050000037b000000080075050000037c000000080079050000037d00000008007d050000037e000000080081050000037f0000000800850500000380000000080089050000028504008d050c4870616c6c65745f61646d696e5f7574696c731870616c6c65741043616c6c0404540001c840737761705f617574686f72697469657304013c6e65775f617574686f726974696573b50101e4426f756e6465645665633c3c5420617320436f6e6669673e3a3a417574686f7269747949642c20543a3a4d6178417574686f7269746965733e00000ce85468652065787472696e736963207365747320746865206e657720617574686f72697469657320666f72204175726120636f6e73656e7375732ea04974206973206f6e6c792063616c6c61626c652062792074686520726f6f74206163636f756e742e09015468652065787472696e7369632077696c6c2063616c6c2074686520417572612070616c6c657420746f206368616e67652074686520617574686f7269746965732e547375646f5f7365745f64656661756c745f74616b6504013064656661756c745f74616b659c010c75313600010cd05468652065787472696e7369632073657473207468652064656661756c742074616b6520666f7220746865206e6574776f726b2ea04974206973206f6e6c792063616c6c61626c652062792074686520726f6f74206163636f756e742e15015468652065787472696e7369632077696c6c2063616c6c207468652053756274656e736f722070616c6c657420746f20736574207468652064656661756c742074616b652e587375646f5f7365745f74785f726174655f6c696d697404013474785f726174655f6c696d697418010c75363400020cf85468652065787472696e736963207365747320746865207472616e73616374696f6e2072617465206c696d697420666f7220746865206e6574776f726b2ea04974206973206f6e6c792063616c6c61626c652062792074686520726f6f74206163636f756e742e3d015468652065787472696e7369632077696c6c2063616c6c207468652053756274656e736f722070616c6c657420746f2073657420746865207472616e73616374696f6e2072617465206c696d69742e6c7375646f5f7365745f73657276696e675f726174655f6c696d69740801186e65747569649c010c75313600014873657276696e675f726174655f6c696d697418010c75363400030cdc5468652065787472696e7369632073657473207468652073657276696e672072617465206c696d697420666f722061207375626e65742ee04974206973206f6e6c792063616c6c61626c652062792074686520726f6f74206163636f756e74206f72207375626e6574206f776e65722e2d015468652065787472696e7369632077696c6c2063616c6c207468652053756274656e736f722070616c6c657420746f20736574207468652073657276696e672072617465206c696d69742e5c7375646f5f7365745f6d696e5f646966666963756c74790801186e65747569649c010c7531360001386d696e5f646966666963756c747918010c75363400040cdc5468652065787472696e736963207365747320746865206d696e696d756d20646966666963756c747920666f722061207375626e65742ee04974206973206f6e6c792063616c6c61626c652062792074686520726f6f74206163636f756e74206f72207375626e6574206f776e65722e2d015468652065787472696e7369632077696c6c2063616c6c207468652053756274656e736f722070616c6c657420746f2073657420746865206d696e696d756d20646966666963756c74792e5c7375646f5f7365745f6d61785f646966666963756c74790801186e65747569649c010c7531360001386d61785f646966666963756c747918010c75363400050cdc5468652065787472696e736963207365747320746865206d6178696d756d20646966666963756c747920666f722061207375626e65742ee04974206973206f6e6c792063616c6c61626c652062792074686520726f6f74206163636f756e74206f72207375626e6574206f776e65722e2d015468652065787472696e7369632077696c6c2063616c6c207468652053756274656e736f722070616c6c657420746f2073657420746865206d6178696d756d20646966666963756c74792e707375646f5f7365745f776569676874735f76657273696f6e5f6b65790801186e65747569649c010c75313600014c776569676874735f76657273696f6e5f6b657918010c75363400060ce05468652065787472696e73696320736574732074686520776569676874732076657273696f6e206b657920666f722061207375626e65742ee04974206973206f6e6c792063616c6c61626c652062792074686520726f6f74206163636f756e74206f72207375626e6574206f776e65722e31015468652065787472696e7369632077696c6c2063616c6c207468652053756274656e736f722070616c6c657420746f207365742074686520776569676874732076657273696f6e206b65792e7c7375646f5f7365745f776569676874735f7365745f726174655f6c696d69740801186e65747569649c010c753136000158776569676874735f7365745f726174655f6c696d697418010c75363400070cec5468652065787472696e7369632073657473207468652077656967687473207365742072617465206c696d697420666f722061207375626e65742ee04974206973206f6e6c792063616c6c61626c652062792074686520726f6f74206163636f756e74206f72207375626e6574206f776e65722e3d015468652065787472696e7369632077696c6c2063616c6c207468652053756274656e736f722070616c6c657420746f20736574207468652077656967687473207365742072617465206c696d69742e707375646f5f7365745f61646a7573746d656e745f696e74657276616c0801186e65747569649c010c75313600014c61646a7573746d656e745f696e74657276616c9c010c75313600080ce05468652065787472696e7369632073657473207468652061646a7573746d656e7420696e74657276616c20666f722061207375626e65742e31014974206973206f6e6c792063616c6c61626c652062792074686520726f6f74206163636f756e742c206e6f74206368616e676561626c6520627920746865207375626e6574206f776e65722e31015468652065787472696e7369632077696c6c2063616c6c207468652053756274656e736f722070616c6c657420746f20736574207468652061646a7573746d656e7420696e74657276616c2e647375646f5f7365745f61646a7573746d656e745f616c7068610801186e65747569649c010c75313600014061646a7573746d656e745f616c70686118010c75363400090cd45468652065787472696e7369632073657473207468652061646a7573746d656e7420616c70686120666f722061207375626e65742ee04974206973206f6e6c792063616c6c61626c652062792074686520726f6f74206163636f756e74206f72207375626e6574206f776e65722e25015468652065787472696e7369632077696c6c2063616c6c207468652053756274656e736f722070616c6c657420746f20736574207468652061646a7573746d656e7420616c7068612e647375646f5f7365745f6d61785f7765696768745f6c696d69740801186e65747569649c010c7531360001406d61785f7765696768745f6c696d69749c010c753136000c0cd05468652065787472696e7369632073657473207468652061646a7573746d656e74206265746120666f722061207375626e65742ee04974206973206f6e6c792063616c6c61626c652062792074686520726f6f74206163636f756e74206f72207375626e6574206f776e65722e21015468652065787472696e7369632077696c6c2063616c6c207468652053756274656e736f722070616c6c657420746f20736574207468652061646a7573746d656e7420626574612e607375646f5f7365745f696d6d756e6974795f706572696f640801186e65747569649c010c75313600013c696d6d756e6974795f706572696f649c010c753136000d0cd05468652065787472696e73696320736574732074686520696d6d756e69747920706572696f6420666f722061207375626e65742ee04974206973206f6e6c792063616c6c61626c652062792074686520726f6f74206163636f756e74206f72207375626e6574206f776e65722e21015468652065787472696e7369632077696c6c2063616c6c207468652053756274656e736f722070616c6c657420746f207365742074686520696d6d756e69747920706572696f642e707375646f5f7365745f6d696e5f616c6c6f7765645f776569676874730801186e65747569649c010c75313600014c6d696e5f616c6c6f7765645f776569676874739c010c753136000e0cf05468652065787472696e736963207365747320746865206d696e696d756d20616c6c6f776564207765696768747320666f722061207375626e65742ee04974206973206f6e6c792063616c6c61626c652062792074686520726f6f74206163636f756e74206f72207375626e6574206f776e65722e41015468652065787472696e7369632077696c6c2063616c6c207468652053756274656e736f722070616c6c657420746f2073657420746865206d696e696d756d20616c6c6f77656420776569676874732e647375646f5f7365745f6d61785f616c6c6f7765645f756964730801186e65747569649c010c7531360001406d61785f616c6c6f7765645f756964739c010c753136000f0ce45468652065787472696e736963207365747320746865206d6178696d756d20616c6c6f776564205549447320666f722061207375626e65742ea04974206973206f6e6c792063616c6c61626c652062792074686520726f6f74206163636f756e742e69015468652065787472696e7369632077696c6c2063616c6c207468652053756274656e736f722070616c6c657420746f2073657420746865206d6178696d756d20616c6c6f776564205549447320666f722061207375626e65742e387375646f5f7365745f6b617070610801186e65747569649c010c7531360001146b617070619c010c75313600100ca85468652065787472696e736963207365747320746865206b6170706120666f722061207375626e65742ee04974206973206f6e6c792063616c6c61626c652062792074686520726f6f74206163636f756e74206f72207375626e6574206f776e65722ef85468652065787472696e7369632077696c6c2063616c6c207468652053756274656e736f722070616c6c657420746f2073657420746865206b617070612e307375646f5f7365745f72686f0801186e65747569649c010c75313600010c72686f9c010c75313600110ca05468652065787472696e7369632073657473207468652072686f20666f722061207375626e65742ee04974206973206f6e6c792063616c6c61626c652062792074686520726f6f74206163636f756e74206f72207375626e6574206f776e65722ef05468652065787472696e7369632077696c6c2063616c6c207468652053756274656e736f722070616c6c657420746f20736574207468652072686f2e607375646f5f7365745f61637469766974795f6375746f66660801186e65747569649c010c75313600013c61637469766974795f6375746f66669c010c75313600120cd05468652065787472696e736963207365747320746865206163746976697479206375746f666620666f722061207375626e65742ee04974206973206f6e6c792063616c6c61626c652062792074686520726f6f74206163636f756e74206f72207375626e6574206f776e65722e21015468652065787472696e7369632077696c6c2063616c6c207468652053756274656e736f722070616c6c657420746f2073657420746865206163746976697479206375746f66662e947375646f5f7365745f6e6574776f726b5f726567697374726174696f6e5f616c6c6f7765640801186e65747569649c010c753136000150726567697374726174696f6e5f616c6c6f776564240110626f6f6c00130c05015468652065787472696e736963207365747320746865206e6574776f726b20726567697374726174696f6e20616c6c6f77656420666f722061207375626e65742ee04974206973206f6e6c792063616c6c61626c652062792074686520726f6f74206163636f756e74206f72207375626e6574206f776e65722e55015468652065787472696e7369632077696c6c2063616c6c207468652053756274656e736f722070616c6c657420746f2073657420746865206e6574776f726b20726567697374726174696f6e20616c6c6f7765642ea47375646f5f7365745f6e6574776f726b5f706f775f726567697374726174696f6e5f616c6c6f7765640801186e65747569649c010c753136000150726567697374726174696f6e5f616c6c6f776564240110626f6f6c00140c15015468652065787472696e736963207365747320746865206e6574776f726b20506f5720726567697374726174696f6e20616c6c6f77656420666f722061207375626e65742ee04974206973206f6e6c792063616c6c61626c652062792074686520726f6f74206163636f756e74206f72207375626e6574206f776e65722e65015468652065787472696e7369632077696c6c2063616c6c207468652053756274656e736f722070616c6c657420746f2073657420746865206e6574776f726b20506f5720726567697374726174696f6e20616c6c6f7765642ea87375646f5f7365745f7461726765745f726567697374726174696f6e735f7065725f696e74657276616c0801186e65747569649c010c7531360001847461726765745f726567697374726174696f6e735f7065725f696e74657276616c9c010c75313600150c19015468652065787472696e7369632073657473207468652074617267657420726567697374726174696f6e732070657220696e74657276616c20666f722061207375626e65742ea04974206973206f6e6c792063616c6c61626c652062792074686520726f6f74206163636f756e742e69015468652065787472696e7369632077696c6c2063616c6c207468652053756274656e736f722070616c6c657420746f20736574207468652074617267657420726567697374726174696f6e732070657220696e74657276616c2e447375646f5f7365745f6d696e5f6275726e0801186e65747569649c010c7531360001206d696e5f6275726e18010c75363400160cc45468652065787472696e736963207365747320746865206d696e696d756d206275726e20666f722061207375626e65742ea04974206973206f6e6c792063616c6c61626c652062792074686520726f6f74206163636f756e742e15015468652065787472696e7369632077696c6c2063616c6c207468652053756274656e736f722070616c6c657420746f2073657420746865206d696e696d756d206275726e2e447375646f5f7365745f6d61785f6275726e0801186e65747569649c010c7531360001206d61785f6275726e18010c75363400170cc45468652065787472696e736963207365747320746865206d6178696d756d206275726e20666f722061207375626e65742ee04974206973206f6e6c792063616c6c61626c652062792074686520726f6f74206163636f756e74206f72207375626e6574206f776e65722e15015468652065787472696e7369632077696c6c2063616c6c207468652053756274656e736f722070616c6c657420746f2073657420746865206d6178696d756d206275726e2e4c7375646f5f7365745f646966666963756c74790801186e65747569649c010c753136000128646966666963756c747918010c75363400180cbc5468652065787472696e73696320736574732074686520646966666963756c747920666f722061207375626e65742ee04974206973206f6e6c792063616c6c61626c652062792074686520726f6f74206163636f756e74206f72207375626e6574206f776e65722e0d015468652065787472696e7369632077696c6c2063616c6c207468652053756274656e736f722070616c6c657420746f207365742074686520646966666963756c74792e7c7375646f5f7365745f6d61785f616c6c6f7765645f76616c696461746f72730801186e65747569649c010c7531360001586d61785f616c6c6f7765645f76616c696461746f72739c010c75313600190cfc5468652065787472696e736963207365747320746865206d6178696d756d20616c6c6f7765642076616c696461746f727320666f722061207375626e65742ea04974206973206f6e6c792063616c6c61626c652062792074686520726f6f74206163636f756e742e4d015468652065787472696e7369632077696c6c2063616c6c207468652053756274656e736f722070616c6c657420746f2073657420746865206d6178696d756d20616c6c6f7765642076616c696461746f72732e747375646f5f7365745f626f6e64735f6d6f76696e675f617665726167650801186e65747569649c010c753136000150626f6e64735f6d6f76696e675f6176657261676518010c753634001a0ce45468652065787472696e73696320736574732074686520626f6e6473206d6f76696e67206176657261676520666f722061207375626e65742ee04974206973206f6e6c792063616c6c61626c652062792074686520726f6f74206163636f756e74206f72207375626e6574206f776e65722e35015468652065787472696e7369632077696c6c2063616c6c207468652053756274656e736f722070616c6c657420746f207365742074686520626f6e6473206d6f76696e6720617665726167652e587375646f5f7365745f626f6e64735f70656e616c74790801186e65747569649c010c753136000134626f6e64735f70656e616c74799c010c753136003c0cc85468652065787472696e73696320736574732074686520626f6e64732070656e616c747920666f722061207375626e65742ee04974206973206f6e6c792063616c6c61626c652062792074686520726f6f74206163636f756e74206f72207375626e6574206f776e65722e19015468652065787472696e7369632077696c6c2063616c6c207468652053756274656e736f722070616c6c657420746f207365742074686520626f6e64732070656e616c74792e907375646f5f7365745f6d61785f726567697374726174696f6e735f7065725f626c6f636b0801186e65747569649c010c75313600016c6d61785f726567697374726174696f6e735f7065725f626c6f636b9c010c753136001b0c11015468652065787472696e736963207365747320746865206d6178696d756d20726567697374726174696f6e732070657220626c6f636b20666f722061207375626e65742ea04974206973206f6e6c792063616c6c61626c652062792074686520726f6f74206163636f756e742e61015468652065787472696e7369632077696c6c2063616c6c207468652053756274656e736f722070616c6c657420746f2073657420746865206d6178696d756d20726567697374726174696f6e732070657220626c6f636b2e647375646f5f7365745f7375626e65745f6f776e65725f6375740401407375626e65745f6f776e65725f6375749c010c753136001c0cd45468652065787472696e736963207365747320746865207375626e6574206f776e65722063757420666f722061207375626e65742ea04974206973206f6e6c792063616c6c61626c652062792074686520726f6f74206163636f756e742e25015468652065787472696e7369632077696c6c2063616c6c207468652053756274656e736f722070616c6c657420746f2073657420746865207375626e6574206f776e6572206375742e6c7375646f5f7365745f6e6574776f726b5f726174655f6c696d6974040128726174655f6c696d697418010c753634001d0ce85468652065787472696e736963207365747320746865206e6574776f726b2072617465206c696d697420666f7220746865206e6574776f726b2ea04974206973206f6e6c792063616c6c61626c652062792074686520726f6f74206163636f756e742e2d015468652065787472696e7369632077696c6c2063616c6c207468652053756274656e736f722070616c6c657420746f2073657420746865206e6574776f726b2072617465206c696d69742e387375646f5f7365745f74656d706f0801186e65747569649c010c75313600011474656d706f9c010c753136001e0ca85468652065787472696e7369632073657473207468652074656d706f20666f722061207375626e65742ea04974206973206f6e6c792063616c6c61626c652062792074686520726f6f74206163636f756e742ef85468652065787472696e7369632077696c6c2063616c6c207468652053756274656e736f722070616c6c657420746f20736574207468652074656d706f2e5c7375646f5f7365745f746f74616c5f69737375616e6365040138746f74616c5f69737375616e636518010c75363400210cd85468652065787472696e73696320736574732074686520746f74616c2069737375616e636520666f7220746865206e6574776f726b2ea04974206973206f6e6c792063616c6c61626c652062792074686520726f6f74206163636f756e742e45015468652065787472696e7369632077696c6c2063616c6c207468652053756274656e736f722070616c6c657420746f20736574207468652069737375616e636520666f7220746865206e6574776f726b2e807375646f5f7365745f6e6574776f726b5f696d6d756e6974795f706572696f6404013c696d6d756e6974795f706572696f6418010c75363400230cdc5468652065787472696e73696320736574732074686520696d6d756e69747920706572696f6420666f7220746865206e6574776f726b2ea04974206973206f6e6c792063616c6c61626c652062792074686520726f6f74206163636f756e742e61015468652065787472696e7369632077696c6c2063616c6c207468652053756274656e736f722070616c6c657420746f207365742074686520696d6d756e69747920706572696f6420666f7220746865206e6574776f726b2e787375646f5f7365745f6e6574776f726b5f6d696e5f6c6f636b5f636f73740401246c6f636b5f636f737418010c75363400240cd45468652065787472696e736963207365747320746865206d696e206c6f636b20636f737420666f7220746865206e6574776f726b2ea04974206973206f6e6c792063616c6c61626c652062792074686520726f6f74206163636f756e742e59015468652065787472696e7369632077696c6c2063616c6c207468652053756274656e736f722070616c6c657420746f2073657420746865206d696e206c6f636b20636f737420666f7220746865206e6574776f726b2e547375646f5f7365745f7375626e65745f6c696d697404012c6d61785f7375626e6574739c010c75313600250cd05468652065787472696e736963207365747320746865207375626e6574206c696d697420666f7220746865206e6574776f726b2ea04974206973206f6e6c792063616c6c61626c652062792074686520726f6f74206163636f756e742e15015468652065787472696e7369632077696c6c2063616c6c207468652053756274656e736f722070616c6c657420746f2073657420746865207375626e6574206c696d69742e807375646f5f7365745f6c6f636b5f726564756374696f6e5f696e74657276616c040120696e74657276616c18010c75363400260cfc5468652065787472696e736963207365747320746865206c6f636b20726564756374696f6e20696e74657276616c20666f7220746865206e6574776f726b2ea04974206973206f6e6c792063616c6c61626c652062792074686520726f6f74206163636f756e742e41015468652065787472696e7369632077696c6c2063616c6c207468652053756274656e736f722070616c6c657420746f2073657420746865206c6f636b20726564756374696f6e20696e74657276616c2e547375646f5f7365745f72616f5f72656379636c65640801186e65747569649c010c75313600013072616f5f72656379636c656418010c75363400270cc45468652065787472696e7369632073657473207468652072656379636c65642052414f20666f722061207375626e65742ea04974206973206f6e6c792063616c6c61626c652062792074686520726f6f74206163636f756e742e15015468652065787472696e7369632077696c6c2063616c6c207468652053756274656e736f722070616c6c657420746f20736574207468652072656379636c65642052414f2e607375646f5f7365745f7374616b655f7468726573686f6c640401246d696e5f7374616b6518010c753634002a0ca45468652065787472696e7369632073657473207468652077656967687473206d696e207374616b652ea04974206973206f6e6c792063616c6c61626c652062792074686520726f6f74206163636f756e742e29015468652065787472696e7369632077696c6c2063616c6c207468652053756274656e736f722070616c6c657420746f20736574207468652077656967687473206d696e207374616b652e947375646f5f7365745f6e6f6d696e61746f725f6d696e5f72657175697265645f7374616b650401246d696e5f7374616b6518010c753634002b0cf45468652065787472696e736963207365747320746865206d696e696d756d207374616b6520726571756972656420666f72206e6f6d696e61746f72732ea04974206973206f6e6c792063616c6c61626c652062792074686520726f6f74206163636f756e742e79015468652065787472696e7369632077696c6c2063616c6c207468652053756274656e736f722070616c6c657420746f2073657420746865206d696e696d756d207374616b6520726571756972656420666f72206e6f6d696e61746f72732e907375646f5f7365745f74785f64656c65676174655f74616b655f726174655f6c696d697404013474785f726174655f6c696d697418010c753634002d0c05015468652065787472696e7369632073657473207468652072617465206c696d697420666f722064656c65676174652074616b65207472616e73616374696f6e732ea04974206973206f6e6c792063616c6c61626c652062792074686520726f6f74206163636f756e742e89015468652065787472696e7369632077696c6c2063616c6c207468652053756274656e736f722070616c6c657420746f20736574207468652072617465206c696d697420666f722064656c65676174652074616b65207472616e73616374696f6e732e687375646f5f7365745f6d696e5f64656c65676174655f74616b6504011074616b659c010c753136002e0cb45468652065787472696e736963207365747320746865206d696e696d756d2064656c65676174652074616b652ea04974206973206f6e6c792063616c6c61626c652062792074686520726f6f74206163636f756e742e39015468652065787472696e7369632077696c6c2063616c6c207468652053756274656e736f722070616c6c657420746f2073657420746865206d696e696d756d2064656c65676174652074616b652e987375646f5f7365745f636f6d6d69745f72657665616c5f776569676874735f656e61626c65640801186e65747569649c010c75313600011c656e61626c6564240110626f6f6c00310c05015468652065787472696e73696320656e61626c65642f64697361626c657320636f6d6d69742f7265617665616c20666f72206120676976656e207375626e65742ee04974206973206f6e6c792063616c6c61626c652062792074686520726f6f74206163636f756e74206f72207375626e6574206f776e65722ef85468652065787472696e7369632077696c6c2063616c6c207468652053756274656e736f722070616c6c657420746f20736574207468652076616c75652e747375646f5f7365745f6c69717569645f616c7068615f656e61626c65640801186e65747569649c010c75313600011c656e61626c6564240110626f6f6c003224d0456e61626c6573206f722064697361626c6573204c697175696420416c70686120666f72206120676976656e207375626e65742e00302320506172616d65746572734d012d20606f726967696e603a20546865206f726967696e206f66207468652063616c6c2c207768696368206d7573742062652074686520726f6f74206163636f756e74206f72207375626e6574206f776e65722ec42d20606e6574756964603a2054686520756e69717565206964656e74696669657220666f7220746865207375626e65742ef82d2060656e61626c6564603a204120626f6f6c65616e20666c616720746f20656e61626c65206f722064697361626c65204c697175696420416c7068612e00202320576569676874cd01546869732066756e6374696f6e20686173206120666978656420776569676874206f66203020616e6420697320636c617373696669656420617320616e206f7065726174696f6e616c207472616e73616374696f6e207468617420646f6573206e6f7420696e63757220616e7920666565732e547375646f5f7365745f616c7068615f76616c7565730c01186e65747569649c010c753136000124616c7068615f6c6f779c010c753136000128616c7068615f686967689c010c75313600330470536574732076616c75657320666f72206c697175696420616c706861687375646f5f7365745f6e6574776f726b5f6d61785f7374616b650801186e65747569649c010c7531360001246d61785f7374616b6518010c753634003564d85365747320746865206d6178696d756d207374616b6520616c6c6f77656420666f722061207370656369666963206e6574776f726b2e004d01546869732066756e6374696f6e20616c6c6f77732074686520726f6f74206163636f756e7420746f2073657420746865206d6178696d756d207374616b6520666f72206120676976656e206e6574776f726b2e05014974207570646174657320746865206e6574776f726b2773206d6178696d756d207374616b652076616c756520616e64206c6f677320746865206368616e67652e002c2320417267756d656e74730011012a20606f726967696e60202d20546865206f726967696e206f66207468652063616c6c2c207768696368206d7573742062652074686520726f6f74206163636f756e742ec82a20606e657475696460202d2054686520756e69717565206964656e746966696572206f6620746865206e6574776f726b2ecc2a20606d61785f7374616b6560202d20546865206e6577206d6178696d756d207374616b652076616c756520746f207365742e0024232052657475726e7300250152657475726e7320604f6b282829296020696620746865206f7065726174696f6e206973207375636365737366756c2c206f7220616e206572726f72206966206974206661696c732e002423204578616d706c6500001c23204e6f74657300dc2d20546869732066756e6374696f6e2063616e206f6e6c792062652063616c6c65642062792074686520726f6f74206163636f756e742ee02d2054686520606e6574756964602073686f756c6420636f72726573706f6e6420746f20616e206578697374696e67206e6574776f726b2e00182320544f444f009c7375646f5f7365745f636f6c646b65795f737761705f7363686564756c655f6475726174696f6e0401206475726174696f6e100144426c6f636b4e756d626572466f723c543e003638bc5365747320746865206475726174696f6e206f662074686520636f6c646b65792073776170207363686564756c652e006501546869732065787472696e73696320616c6c6f77732074686520726f6f74206163636f756e7420746f2073657420746865206475726174696f6e20666f722074686520636f6c646b65792073776170207363686564756c652e810154686520636f6c646b65792073776170207363686564756c652064657465726d696e657320686f77206c6f6e672069742074616b657320666f72206120636f6c646b65792073776170206f7065726174696f6e20746f20636f6d706c6574652e002c2320417267756d656e747311012a20606f726967696e60202d20546865206f726967696e206f66207468652063616c6c2c207768696368206d7573742062652074686520726f6f74206163636f756e742e4d012a20606475726174696f6e60202d20546865206e6577206475726174696f6e20666f722074686520636f6c646b65792073776170207363686564756c652c20696e206e756d626572206f6620626c6f636b732e002023204572726f7273d82a20604261644f726967696e60202d204966207468652063616c6c6572206973206e6f742074686520726f6f74206163636f756e742e00202320576569676874dc5765696768742069732068616e646c6564206279207468652060235b70616c6c65743a3a7765696768745d60206174747269627574652eac7375646f5f7365745f646973736f6c76655f6e6574776f726b5f7363686564756c655f6475726174696f6e0401206475726174696f6e100144426c6f636b4e756d626572466f723c543e003738cc5365747320746865206475726174696f6e206f662074686520646973736f6c7665206e6574776f726b207363686564756c652e007501546869732065787472696e73696320616c6c6f77732074686520726f6f74206163636f756e7420746f2073657420746865206475726174696f6e20666f722074686520646973736f6c7665206e6574776f726b207363686564756c652ead0154686520646973736f6c7665206e6574776f726b207363686564756c652064657465726d696e657320686f77206c6f6e672069742074616b657320666f722061206e6574776f726b20646973736f6c7574696f6e206f7065726174696f6e20746f20636f6d706c6574652e002c2320417267756d656e747311012a20606f726967696e60202d20546865206f726967696e206f66207468652063616c6c2c207768696368206d7573742062652074686520726f6f74206163636f756e742e5d012a20606475726174696f6e60202d20546865206e6577206475726174696f6e20666f722074686520646973736f6c7665206e6574776f726b207363686564756c652c20696e206e756d626572206f6620626c6f636b732e002023204572726f7273d82a20604261644f726967696e60202d204966207468652063616c6c6572206973206e6f742074686520726f6f74206163636f756e742e00202320576569676874dc5765696768742069732068616e646c6564206279207468652060235b70616c6c65743a3a7765696768745d60206174747269627574652e9c7375646f5f7365745f636f6d6d69745f72657665616c5f776569676874735f696e74657276616c0801186e65747569649c010c753136000120696e74657276616c18010c753634003940f4536574732074686520636f6d6d69742d72657665616c207765696768747320706572696f647320666f722061207370656369666963207375626e65742e001d02546869732065787472696e73696320616c6c6f777320746865207375626e6574206f776e6572206f7220726f6f74206163636f756e7420746f2073657420746865206475726174696f6e2028696e2065706f6368732920647572696e6720776869636820636f6d6d69747465642077656967687473206d7573742062652072657665616c65642ee10154686520636f6d6d69742d72657665616c206d656368616e69736d20656e7375726573207468617420757365727320636f6d6d6974207765696768747320696e20616476616e636520616e642072657665616c207468656d206f6e6c792077697468696e20612073706563696669656420706572696f642e002c2320417267756d656e747361012a20606f726967696e60202d20546865206f726967696e206f66207468652063616c6c2c207768696368206d75737420626520746865207375626e6574206f776e6572206f722074686520726f6f74206163636f756e742e55012a20606e657475696460202d2054686520756e69717565206964656e746966696572206f6620746865207375626e657420666f722077686963682074686520706572696f647320617265206265696e67207365742e21012a2060706572696f647360202d20546865206e756d626572206f662065706f636873207468617420646566696e652074686520636f6d6d69742d72657665616c20706572696f642e002023204572726f72733d012a20604261644f726967696e60202d204966207468652063616c6c6572206973206e65697468657220746865207375626e6574206f776e6572206e6f722074686520726f6f74206163636f756e742e01012a20605375626e6574446f65734e6f74457869737460202d2049662074686520737065636966696564207375626e657420646f6573206e6f742065786973742e00202320576569676874dc5765696768742069732068616e646c6564206279207468652060235b70616c6c65743a3a7765696768745d60206174747269627574652e547375646f5f7365745f65766d5f636861696e5f6964040120636861696e5f696418010c753634003a2c5453657473207468652045564d20436861696e49442e002c2320417267756d656e747361012a20606f726967696e60202d20546865206f726967696e206f66207468652063616c6c2c207768696368206d75737420626520746865207375626e6574206f776e6572206f722074686520726f6f74206163636f756e742e782a2060636861696e496460202d205468652075363420636861696e204944002023204572726f72733d012a20604261644f726967696e60202d204966207468652063616c6c6572206973206e65697468657220746865207375626e6574206f776e6572206e6f722074686520726f6f74206163636f756e742e00202320576569676874dc5765696768742069732068616e646c6564206279207468652060235b70616c6c65743a3a7765696768745d60206174747269627574652e5c7363686564756c655f6772616e6470615f6368616e67650c01406e6578745f617574686f726974696573800134417574686f726974794c697374000124696e5f626c6f636b73100144426c6f636b4e756d626572466f723c543e000118666f72636564d10101644f7074696f6e3c426c6f636b4e756d626572466f723c543e3e003b3c250141207075626c696320696e7465726661636520666f72206070616c6c65745f6772616e6470613a3a50616c6c65743a3a7363686564756c655f6772616e6470615f6368616e6765602e00945363686564756c652061206368616e676520696e2074686520617574686f7269746965732e005501546865206368616e67652077696c6c206265206170706c6965642061742074686520656e64206f6620657865637574696f6e206f662074686520626c6f636b2060696e5f626c6f636b736020616674657220746865550163757272656e7420626c6f636b2e20546869732076616c7565206d617920626520302c20696e207768696368206361736520746865206368616e6765206973206170706c6965642061742074686520656e64206f66487468652063757272656e7420626c6f636b2e0049014966207468652060666f726365646020706172616d6574657220697320646566696e65642c207468697320696e646963617465732074686174207468652063757272656e742073657420686173206265656e490173796e6368726f6e6f75736c792064657465726d696e656420746f206265206f66666c696e6520616e6420746861742061667465722060696e5f626c6f636b73602074686520676976656e206368616e67654d0173686f756c64206265206170706c6965642e2054686520676976656e20626c6f636b206e756d62657220696e6469636174657320746865206d656469616e206c6173742066696e616c697a656420626c6f636b51016e756d62657220616e642069742073686f756c642062652075736564206173207468652063616e6f6e20626c6f636b207768656e207374617274696e6720746865206e6577206772616e64706120766f7465722e0059014e6f206368616e67652073686f756c64206265207369676e616c6564207768696c6520616e79206368616e67652069732070656e64696e672e2052657475726e7320616e206572726f722069662061206368616e67654c697320616c72656164792070656e64696e672e607375646f5f7365745f746f67676c655f7472616e736665720801186e65747569649c010c753136000118746f67676c65240110626f6f6c003d24d0456e61626c6573206f722064697361626c6573204c697175696420416c70686120666f72206120676976656e207375626e65742e00302320506172616d65746572734d012d20606f726967696e603a20546865206f726967696e206f66207468652063616c6c2c207768696368206d7573742062652074686520726f6f74206163636f756e74206f72207375626e6574206f776e65722ec42d20606e6574756964603a2054686520756e69717565206964656e74696669657220666f7220746865207375626e65742ef82d2060656e61626c6564603a204120626f6f6c65616e20666c616720746f20656e61626c65206f722064697361626c65204c697175696420416c7068612e00202320576569676874cd01546869732066756e6374696f6e20686173206120666978656420776569676874206f66203020616e6420697320636c617373696669656420617320616e206f7065726174696f6e616c207472616e73616374696f6e207468617420646f6573206e6f7420696e63757220616e7920666565732e046501446973706174636861626c652066756e6374696f6e7320616c6c6f777320757365727320746f20696e7465726163742077697468207468652070616c6c657420616e6420696e766f6b65207374617465206368616e6765732e91050c4070616c6c65745f736166655f6d6f64651870616c6c65741043616c6c04045400012014656e7465720000181901456e74657220736166652d6d6f6465207065726d697373696f6e6c6573736c7920666f72205b60436f6e6669673a3a456e7465724475726174696f6e605d20626c6f636b732e0009015265736572766573205b60436f6e6669673a3a456e7465724465706f736974416d6f756e74605d2066726f6d207468652063616c6c65722773206163636f756e742eb4456d69747320616e205b604576656e743a3a456e7465726564605d206576656e74206f6e20737563636573732e0d014572726f72732077697468205b604572726f723a3a456e7465726564605d2069662074686520736166652d6d6f646520697320616c726561647920656e74657265642e15014572726f72732077697468205b604572726f723a3a4e6f74436f6e66696775726564605d20696620746865206465706f73697420616d6f756e7420697320604e6f6e65602e2c666f7263655f656e7465720001181901456e74657220736166652d6d6f646520627920666f72636520666f722061207065722d6f726967696e20636f6e66696775726564206e756d626572206f6620626c6f636b732e00b4456d69747320616e205b604576656e743a3a456e7465726564605d206576656e74206f6e20737563636573732e0d014572726f72732077697468205b604572726f723a3a456e7465726564605d2069662074686520736166652d6d6f646520697320616c726561647920656e74657265642e00f843616e206f6e6c792062652063616c6c656420627920746865205b60436f6e6669673a3a466f726365456e7465724f726967696e605d206f726967696e2e18657874656e6400022c3101457874656e642074686520736166652d6d6f6465207065726d697373696f6e6c6573736c7920666f72205b60436f6e6669673a3a457874656e644475726174696f6e605d20626c6f636b732e00e85468697320616363756d756c61746573206f6e20746f70206f66207468652063757272656e742072656d61696e696e67206475726174696f6e2e0d015265736572766573205b60436f6e6669673a3a457874656e644465706f736974416d6f756e74605d2066726f6d207468652063616c6c65722773206163636f756e742eb8456d69747320616e205b604576656e743a3a457874656e646564605d206576656e74206f6e20737563636573732ee84572726f72732077697468205b604572726f723a3a457869746564605d2069662074686520736166652d6d6f646520697320656e74657265642e15014572726f72732077697468205b604572726f723a3a4e6f74436f6e66696775726564605d20696620746865206465706f73697420616d6f756e7420697320604e6f6e65602e00450154686973206d61792062652063616c6c656420627920616e79207369676e6564206f726967696e2077697468205b60436f6e6669673a3a457874656e644465706f736974416d6f756e74605d2066726565350163757272656e637920746f20726573657276652e20546869732063616c6c2063616e2062652064697361626c656420666f7220616c6c206f726967696e7320627920636f6e6669677572696e67a85b60436f6e6669673a3a457874656e644465706f736974416d6f756e74605d20746f20604e6f6e65602e30666f7263655f657874656e640003182d01457874656e642074686520736166652d6d6f646520627920666f72636520666f722061207065722d6f726967696e20636f6e66696775726564206e756d626572206f6620626c6f636b732e00b8456d69747320616e205b604576656e743a3a457874656e646564605d206576656e74206f6e20737563636573732eec4572726f72732077697468205b604572726f723a3a457869746564605d2069662074686520736166652d6d6f646520697320696e6163746976652e00fc43616e206f6e6c792062652063616c6c656420627920746865205b60436f6e6669673a3a466f726365457874656e644f726967696e605d206f726967696e2e28666f7263655f65786974000424604578697420736166652d6d6f646520627920666f7263652e001d01456d69747320616e205b604576656e743a3a457869746564605d2077697468205b6045786974526561736f6e3a3a466f726365605d206576656e74206f6e20737563636573732eec4572726f72732077697468205b604572726f723a3a457869746564605d2069662074686520736166652d6d6f646520697320696e6163746976652e0055014e6f74653a2060736166652d6d6f6465602077696c6c206265206175746f6d61746963616c6c79206465616374697661746564206279205b6050616c6c65743a3a6f6e5f696e697469616c697a65605d20686f6f6b250161667465722074686520626c6f636b206865696768742069732067726561746572207468616e20746865205b60456e7465726564556e74696c605d2073746f72616765206974656d2e5501456d69747320616e205b604576656e743a3a457869746564605d2077697468205b6045786974526561736f6e3a3a54696d656f7574605d206576656e74207768656e20646561637469766174656420696e2074686514686f6f6b2e4c666f7263655f736c6173685f6465706f73697408011c6163636f756e74000130543a3a4163636f756e744964000114626c6f636b100144426c6f636b4e756d626572466f723c543e0005243101536c6173682061206465706f73697420666f7220616e206163636f756e74207468617420656e7465726564206f7220657874656e64656420736166652d6d6f6465206174206120676976656e44686973746f726963616c20626c6f636b2e00cc546869732063616e206f6e6c792062652063616c6c6564207768696c6520736166652d6d6f646520697320656e74657265642e00cc456d6974732061205b604576656e743a3a4465706f736974536c6173686564605d206576656e74206f6e20737563636573732edc4572726f72732077697468205b604572726f723a3a456e7465726564605d20696620736166652d6d6f646520697320656e74657265642e00010143616e206f6e6c792062652063616c6c656420627920746865205b60436f6e6669673a3a466f7263654465706f7369744f726967696e605d206f726967696e2e3c72656c656173655f6465706f73697408011c6163636f756e74000130543a3a4163636f756e744964000114626c6f636b100144426c6f636b4e756d626572466f723c543e00063035015065726d697373696f6e6c6573736c792072656c656173652061206465706f73697420666f7220616e206163636f756e74207468617420656e746572656420736166652d6d6f646520617420615c676976656e20686973746f726963616c20626c6f636b2e0049015468652063616c6c2063616e20626520636f6d706c6574656c792064697361626c65642062792073657474696e67205b60436f6e6669673a3a52656c6561736544656c6179605d20746f20604e6f6e65602ef8546869732063616e6e6f742062652063616c6c6564207768696c6520736166652d6d6f646520697320656e746572656420616e64206e6f7420756e74696c21015b60436f6e6669673a3a52656c6561736544656c6179605d20626c6f636b732068617665207061737365642073696e636520736166652d6d6f64652077617320656e74657265642e00d0456d6974732061205b604576656e743a3a4465706f73697452656c6561736564605d206576656e74206f6e20737563636573732eec4572726f72732077697468205b604572726f723a3a456e7465726564605d2069662074686520736166652d6d6f646520697320656e74657265642e49014572726f72732077697468205b604572726f723a3a43616e6e6f7452656c65617365596574605d206966205b60436f6e6669673a3a52656c6561736544656c6179605d20626c6f636b2068617665206e6f7461017061737365642073696e636520736166652d6d6f64652077617320656e74657265642e204572726f72732077697468205b604572726f723a3a4e6f4465706f736974605d2069662074686520706179656520686173206e6fa472657365727665642063757272656e63792061742074686520626c6f636b207370656369666965642e54666f7263655f72656c656173655f6465706f73697408011c6163636f756e74000130543a3a4163636f756e744964000114626c6f636b100144426c6f636b4e756d626572466f723c543e00072c2d01466f72636520746f2072656c656173652061206465706f73697420666f7220616e206163636f756e74207468617420656e746572656420736166652d6d6f6465206174206120676976656e44686973746f726963616c20626c6f636b2e00d0546869732063616e2062652063616c6c6564207768696c6520736166652d6d6f6465206973207374696c6c20656e74657265642e00d0456d6974732061205b604576656e743a3a4465706f73697452656c6561736564605d206576656e74206f6e20737563636573732edc4572726f72732077697468205b604572726f723a3a456e7465726564605d20696620736166652d6d6f646520697320656e74657265642e35014572726f72732077697468205b604572726f723a3a4e6f4465706f736974605d2069662074686520706179656520686173206e6f2072657365727665642063757272656e6379206174207468654073706563696669656420626c6f636b2e00010143616e206f6e6c792062652063616c6c656420627920746865205b60436f6e6669673a3a466f7263654465706f7369744f726967696e605d206f726967696e2e040d01436f6e7461696e7320612076617269616e742070657220646973706174636861626c652065787472696e736963207468617420746869732070616c6c6574206861732e95050c3c70616c6c65745f657468657265756d1870616c6c65741043616c6c040454000104207472616e7361637404012c7472616e73616374696f6e9905012c5472616e73616374696f6e000004845472616e7361637420616e20457468657265756d207472616e73616374696f6e2e040d01436f6e7461696e7320612076617269616e742070657220646973706174636861626c652065787472696e736963207468617420746869732070616c6c6574206861732e99050c20657468657265756d2c7472616e73616374696f6e345472616e73616374696f6e563200010c184c656761637904009d0501444c65676163795472616e73616374696f6e0000001c454950323933300400ad050148454950323933305472616e73616374696f6e0001001c454950313535390400b9050148454950313535395472616e73616374696f6e000200009d050c20657468657265756d2c7472616e73616374696f6e444c65676163795472616e73616374696f6e00001c01146e6f6e636541010110553235360001246761735f707269636541010110553235360001246761735f6c696d69744101011055323536000118616374696f6ea10501445472616e73616374696f6e416374696f6e00011476616c75654101011055323536000114696e70757438011442797465730001247369676e6174757265a50501505472616e73616374696f6e5369676e61747572650000a1050c20657468657265756d2c7472616e73616374696f6e445472616e73616374696f6e416374696f6e0001081043616c6c04000d010110483136300000001843726561746500010000a5050c20657468657265756d2c7472616e73616374696f6e505472616e73616374696f6e5369676e617475726500000c010476a90501545472616e73616374696f6e5265636f766572794964000104723401104832353600010473340110483235360000a9050c20657468657265756d2c7472616e73616374696f6e545472616e73616374696f6e5265636f7665727949640000040018010c7536340000ad050c20657468657265756d2c7472616e73616374696f6e48454950323933305472616e73616374696f6e00002c0120636861696e5f696418010c7536340001146e6f6e636541010110553235360001246761735f707269636541010110553235360001246761735f6c696d69744101011055323536000118616374696f6ea10501445472616e73616374696f6e416374696f6e00011476616c75654101011055323536000114696e707574380114427974657300012c6163636573735f6c697374b10501284163636573734c6973740001306f64645f795f706172697479240110626f6f6c000104723401104832353600010473340110483235360000b105000002b50500b5050c20657468657265756d2c7472616e73616374696f6e384163636573734c6973744974656d000008011c616464726573730d01011c4164647265737300013073746f726167655f6b657973b401245665633c483235363e0000b9050c20657468657265756d2c7472616e73616374696f6e48454950313535395472616e73616374696f6e0000300120636861696e5f696418010c7536340001146e6f6e636541010110553235360001606d61785f7072696f726974795f6665655f7065725f676173410101105532353600013c6d61785f6665655f7065725f67617341010110553235360001246761735f6c696d69744101011055323536000118616374696f6ea10501445472616e73616374696f6e416374696f6e00011476616c75654101011055323536000114696e707574380114427974657300012c6163636573735f6c697374b10501284163636573734c6973740001306f64645f795f706172697479240110626f6f6c000104723401104832353600010473340110483235360000bd050c2870616c6c65745f65766d1870616c6c65741043616c6c04045400011820776974686472617708011c616464726573730d0101104831363000011476616c756518013042616c616e63654f663c543e000004e057697468647261772062616c616e63652066726f6d2045564d20696e746f2063757272656e63792f62616c616e6365732070616c6c65742e1063616c6c240118736f757263650d010110483136300001187461726765740d01011048313630000114696e70757438011c5665633c75383e00011476616c756541010110553235360001246761735f6c696d697418010c75363400013c6d61785f6665655f7065725f67617341010110553235360001606d61785f7072696f726974795f6665655f7065725f676173c10501304f7074696f6e3c553235363e0001146e6f6e6365c10501304f7074696f6e3c553235363e00012c6163636573735f6c697374c50501585665633c28483136302c205665633c483235363e293e0001045d01497373756520616e2045564d2063616c6c206f7065726174696f6e2e20546869732069732073696d696c617220746f2061206d6573736167652063616c6c207472616e73616374696f6e20696e20457468657265756d2e18637265617465200118736f757263650d01011048313630000110696e697438011c5665633c75383e00011476616c756541010110553235360001246761735f6c696d697418010c75363400013c6d61785f6665655f7065725f67617341010110553235360001606d61785f7072696f726974795f6665655f7065725f676173c10501304f7074696f6e3c553235363e0001146e6f6e6365c10501304f7074696f6e3c553235363e00012c6163636573735f6c697374c50501585665633c28483136302c205665633c483235363e293e0002085101497373756520616e2045564d20637265617465206f7065726174696f6e2e20546869732069732073696d696c617220746f206120636f6e7472616374206372656174696f6e207472616e73616374696f6e20696e24457468657265756d2e1c63726561746532240118736f757263650d01011048313630000110696e697438011c5665633c75383e00011073616c743401104832353600011476616c756541010110553235360001246761735f6c696d697418010c75363400013c6d61785f6665655f7065725f67617341010110553235360001606d61785f7072696f726974795f6665655f7065725f676173c10501304f7074696f6e3c553235363e0001146e6f6e6365c10501304f7074696f6e3c553235363e00012c6163636573735f6c697374c50501585665633c28483136302c205665633c483235363e293e0003047c497373756520616e2045564d2063726561746532206f7065726174696f6e2e347365745f77686974656c69737404010c6e6577cd0501245665633c483136303e0004004464697361626c655f77686974656c69737404012064697361626c6564240110626f6f6c000500040d01436f6e7461696e7320612076617269616e742070657220646973706174636861626c652065787472696e736963207468617420746869732070616c6c6574206861732ec10504184f7074696f6e0404540141010108104e6f6e6500000010536f6d65040041010000010000c505000002c90500c905000004080d01b400cd050000020d0100d1050c3c70616c6c65745f626173655f6665651870616c6c65741043616c6c040454000108507365745f626173655f6665655f7065725f67617304010c6665654101011055323536000000387365745f656c6173746963697479040128656c61737469636974794901011c5065726d696c6c000100040d01436f6e7461696e7320612076617269616e742070657220646973706174636861626c652065787472696e736963207468617420746869732070616c6c6574206861732ed5050c3070616c6c65745f6472616e641870616c6c65741043616c6c0404540001082c77726974655f70756c736508013870756c7365735f7061796c6f6164d90501ac50756c7365735061796c6f61643c543a3a5075626c69632c20426c6f636b4e756d626572466f723c543e3e0001247369676e6174757265f10501504f7074696f6e3c543a3a5369676e61747572653e000004e456657269667920616e6420777269746520612070756c73652066726f6d2074686520626561636f6e20696e746f207468652072756e74696d65447365745f626561636f6e5f636f6e666967080138636f6e6669675f7061796c6f6164f90501e0426561636f6e436f6e66696775726174696f6e5061796c6f61643c543a3a5075626c69632c20426c6f636b4e756d626572466f723c543e3e0001247369676e6174757265f10501504f7074696f6e3c543a3a5369676e61747572653e000118d0616c6c6f77732074686520726f6f74207573657220746f207365742074686520626561636f6e20636f6e66696775726174696f6efc67656e6572616c6c79207468697320776f756c642062652063616c6c65642066726f6d20616e206f6666636861696e20776f726b657220636f6e746578742e11017468657265206973206e6f20766572696669636174696f6e206f6620636f6e66696775726174696f6e732c20736f206265206361726566756c207769746820746869732e00642a20606f726967696e603a2074686520726f6f742075736572902a2060636f6e666967603a2074686520626561636f6e20636f6e66696775726174696f6e040d01436f6e7461696e7320612076617269616e742070657220646973706174636861626c652065787472696e736963207468617420746869732070616c6c6574206861732ed9050c3070616c6c65745f6472616e641474797065733450756c7365735061796c6f616408185075626c696301dd052c426c6f636b4e756d6265720110000c0130626c6f636b5f6e756d62657210012c426c6f636b4e756d62657200011870756c736573e10501285665633c50756c73653e0001187075626c6963dd0501185075626c69630000dd05082873705f72756e74696d652c4d756c74695369676e657200010c1c45643235353139040004013c656432353531393a3a5075626c69630000001c53723235353139040004013c737232353531393a3a5075626c69630001001445636473610400f503013465636473613a3a5075626c696300020000e105000002e50500e5050c3070616c6c65745f6472616e641474797065731450756c736500000c0114726f756e6418012c526f756e644e756d62657200012872616e646f6d6e657373e9050170426f756e6465645665633c75382c20436f6e73745533323c33323e3e0001247369676e6174757265ed050174426f756e6465645665633c75382c20436f6e73745533323c3134343e3e0000e9050c4c626f756e6465645f636f6c6c656374696f6e732c626f756e6465645f76656328426f756e64656456656308045401080453000004003801185665633c543e0000ed050c4c626f756e6465645f636f6c6c656374696f6e732c626f756e6465645f76656328426f756e64656456656308045401080453000004003801185665633c543e0000f10504184f7074696f6e04045401f5050108104e6f6e6500000010536f6d650400f5050000010000f505082873705f72756e74696d65384d756c74695369676e617475726500010c1c456432353531390400ed010148656432353531393a3a5369676e61747572650000001c537232353531390400ed010148737232353531393a3a5369676e617475726500010014456364736104008904014065636473613a3a5369676e617475726500020000f9050c3070616c6c65745f6472616e6414747970657368426561636f6e436f6e66696775726174696f6e5061796c6f616408185075626c696301dd052c426c6f636b4e756d6265720110000c0130626c6f636b5f6e756d62657210012c426c6f636b4e756d626572000118636f6e666967fd05014c426561636f6e436f6e66696775726174696f6e0001187075626c6963dd0501185075626c69630000fd050c3070616c6c65745f6472616e641474797065734c426561636f6e436f6e66696775726174696f6e00001c01287075626c69635f6b65790106013c4f70617175655075626c69634b6579000118706572696f6410010c75333200013067656e657369735f74696d6510010c75333200011068617368e905012c426f756e6465644861736800012867726f75705f68617368e905012c426f756e64656448617368000124736368656d655f6964e905012c426f756e646564486173680001206d65746164617461050601204d65746164617461000001060c4c626f756e6465645f636f6c6c656374696f6e732c626f756e6465645f76656328426f756e64656456656308045401080453000004003801185665633c543e000005060c3070616c6c65745f6472616e64147479706573204d657461646174610000040124626561636f6e5f6964e905012c426f756e646564486173680000090604184f7074696f6e04045401ed020108104e6f6e6500000010536f6d650400ed0200000100000d060c4070616c6c65745f73756274656e736f721870616c6c6574144572726f7204045400015901585375624e6574776f726b446f65734e6f74457869737400000468546865207375626e657420646f6573206e6f742065786973742e5c526f6f744e6574776f726b446f65734e6f7445786973740001048054686520726f6f74206e6574776f726b20646f6573206e6f742065786973742e34496e76616c69644970547970650002043901546865207573657220697320747279696e6720746f20736572766520616e2061786f6e207768696368206973206e6f74206f662074797065203420284950763429206f722036202849507636292e40496e76616c6964497041646472657373000304d8416e20696e76616c696420495020616464726573732069732070617373656420746f207468652073657276652066756e6374696f6e2e2c496e76616c6964506f7274000404c0416e20696e76616c696420706f72742069732070617373656420746f207468652073657276652066756e6374696f6e2e6c486f744b65794e6f7452656769737465726564496e5375624e65740005049854686520686f746b6579206973206e6f74207265676973746572656420696e207375626e657458486f744b65794163636f756e744e6f744578697374730006046854686520686f746b657920646f6573206e6f742065786973747370486f744b65794e6f7452656769737465726564496e4e6574776f726b000704ac54686520686f746b6579206973206e6f74207265676973746572656420696e20616e79207375626e65742e504e6f6e4173736f636961746564436f6c644b65790008085d015265717565737420746f207374616b652c20756e7374616b65206f7220737562736372696265206973206d616465206279206120636f6c646b65792074686174206973206e6f74206173736f63696174656420776974684c74686520686f746b6579206163636f756e742e384e6f74456e6f7567685374616b65000908b4444550524543415445443a205374616b6520616d6f756e7420746f207769746864726177206973207a65726f2ef85468652063616c6c657220646f6573206e6f74206861766520656e6f75676874207374616b6520746f20706572666f726d207468697320616374696f6e2e604e6f74456e6f7567685374616b65546f5769746864726177000a0859015468652063616c6c65722069732072657175657374696e672072656d6f76696e67206d6f7265207374616b65207468616e2074686572652065786973747320696e20746865207374616b696e67206163636f756e742e605365653a20225b72656d6f76655f7374616b6528295d222e684e6f74456e6f7567685374616b65546f53657457656967687473000b0849015468652063616c6c65722069732072657175657374696e6720746f20736574207765696768747320627574207468652063616c6c657220686173206c657373207468616e206d696e696d756d207374616b65d0726571756972656420746f20736574207765696768747320286c657373207468616e20576569676874734d696e5374616b65292e704e6f74456e6f7567685374616b65546f5365744368696c646b657973000c04050154686520706172656e7420686f746b657920646f65736e2774206861766520656e6f756768206f776e207374616b6520746f20736574206368696c646b6579732e5c4e6f74456e6f75676842616c616e6365546f5374616b65000d0851015468652063616c6c65722069732072657175657374696e6720616464696e67206d6f7265207374616b65207468616e2074686572652065786973747320696e2074686520636f6c646b6579206163636f756e742e505365653a20225b6164645f7374616b6528295d225842616c616e63655769746864726177616c4572726f72000e0861015468652063616c6c657220697320747279696e6720746f20616464207374616b652c2062757420666f7220736f6d6520726561736f6e207468652072657175657374656420616d6f756e7420636f756c64206e6f742062658c77697468647261776e2066726f6d2074686520636f6c646b6579206163636f756e742e645a65726f42616c616e6365416674657257697468647261776e000f084501556e7375636365737366756c6c792077697468647261772c2062616c616e636520636f756c64206265207a65726f202863616e206e6f74206d616b65206163636f756e74206578697374292061667465722c7769746864726177616c2e5c4e6575726f6e4e6f56616c696461746f725065726d697400100455015468652063616c6c657220697320617474656d7074696e6720746f20736574206e6f6e2d73656c66207765696768747320776974686f7574206265696e672061207065726d69747465642076616c696461746f722e545765696768745665634e6f74457175616c53697a6500110845015468652063616c6c657220697320617474656d7074696e6720746f207365742074686520776569676874206b65797320616e642076616c7565732062757420746865736520766563746f727320686176653c646966666572656e742073697a652e344475706c69636174655569647300120445015468652063616c6c657220697320617474656d7074696e6720746f2073657420776569676874732077697468206475706c6963617465205549447320696e2074686520776569676874206d61747269782e5c556964566563436f6e7461696e496e76616c69644f6e6500130855015468652063616c6c657220697320617474656d7074696e6720746f207365742077656967687420746f206174206c65617374206f6e6520554944207468617420646f6573206e6f7420657869737420696e20746865286d65746167726170682e505765696768745665634c656e67746849734c6f77001404610154686520646973706174636820697320617474656d7074696e6720746f207365742077656967687473206f6e20636861696e207769746820666577657220656c656d656e7473207468616e2061726520616c6c6f7765642e74546f6f4d616e79526567697374726174696f6e7354686973426c6f636b0015084d014e756d626572206f6620726567697374726174696f6e7320696e207468697320626c6f636b20657863656564732074686520616c6c6f776564206e756d6265722028692e652e2c206578636565647320746865b07375626e6574206879706572706172616d6574657220226d61785f726567735f7065725f626c6f636b22292e7c486f744b6579416c726561647952656769737465726564496e5375624e657400160455015468652063616c6c65722069732072657175657374696e67207265676973746572696e672061206e6575726f6e20776869636820616c72656164792065786973747320696e2074686520616374697665207365742e584e6577486f744b6579497353616d65576974684f6c6400170494546865206e657720686f746b6579206973207468652073616d65206173206f6c64206f6e6540496e76616c6964576f726b426c6f636b001804e454686520737570706c69656420506f57206861736820626c6f636b20697320696e2074686520667574757265206f72206e656761746976652e44496e76616c6964446966666963756c7479001904050154686520737570706c69656420506f57206861736820626c6f636b20646f6573206e6f74206d65657420746865206e6574776f726b20646966666963756c74792e2c496e76616c69645365616c001a04f054686520737570706c69656420506f572068617368207365616c20646f6573206e6f74206d617463682074686520737570706c69656420776f726b2e444d61785765696768744578636565646564001b08490154686520646973706174636820697320617474656d7074696e6720746f207365742077656967687473206f6e20636861696e2077697468207765696768742076616c756520657863656564696e6720746865e04d61785765696768744c696d697420286d61785f7765696768745f6c696d6974207375626e6574206879706572706172616d65746572292e54486f744b6579416c726561647944656c6567617465001c04510154686520686f746b657920697320617474656d7074696e6720746f206265636f6d6520612064656c6567617465207768656e2074686520686f746b657920697320616c726561647920612064656c65676174652e5453657474696e6757656967687473546f6f46617374001d04e441207472616e736163746f72206578636565646564207468652072617465206c696d697420666f722073657474696e6720776569676874732e64496e636f727265637457656967687456657273696f6e4b6579001e046101412076616c696461746f7220697320617474656d7074696e6720746f2073657420776569676874732066726f6d20612076616c696461746f72207769746820696e636f7272656374207765696768742076657273696f6e2e6053657276696e67526174654c696d69744578636565646564001f043901416e2061786f6e206f722070726f6d6574686575732073657276696e67206578636565646564207468652072617465206c696d697420666f7220612072656769737465726564206e6575726f6e2e70556964734c656e67746845786365656455696473496e5375624e657400200411015468652063616c6c657220697320617474656d7074696e6720746f2073657420776569676874732077697468206d6f72652055494473207468616e20616c6c6f7765642e684e6574776f726b5478526174654c696d69744578636565646564002104050141207472616e736163746f72206578636565646564207468652072617465206c696d697420666f7220616464206e6574776f726b207472616e73616374696f6e2e6c44656c65676174655478526174654c696d69744578636565646564002204f841207472616e736163746f72206578636565646564207468652072617465206c696d697420666f722064656c6567617465207472616e73616374696f6e2e70486f744b65795365745478526174654c696d69744578636565646564002304110141207472616e736163746f72206578636565646564207468652072617465206c696d697420666f722073657474696e67206f72207377617070696e6720686f746b65792e605374616b696e67526174654c696d69744578636565646564002404c441207472616e736163746f72206578636565646564207468652072617465206c696d697420666f72207374616b696e672e685375624e6574526567697374726174696f6e44697361626c656400250464526567697374726174696f6e2069732064697361626c65642e80546f6f4d616e79526567697374726174696f6e7354686973496e74657276616c0026044101546865206e756d626572206f6620726567697374726174696f6e20617474656d7074732065786365656465642074686520616c6c6f776564206e756d62657220696e2074686520696e74657276616c2e7c5472616e736163746f724163636f756e7453686f756c644265486f744b6579002704a054686520686f746b657920697320726571756972656420746f20626520746865206f726967696e2e3c4e6f7453656e6174654d656d62657200280409014120686f746b657920697320617474656d7074696e6720746f20646f20736f6d657468696e67206f6e6c792073656e617465206d656d626572732063616e20646f2e3846617563657444697361626c65640029044c4661756365742069732064697361626c65642e384e6f745375626e65744f776e6572002a044c4e6f742061207375626e6574206f776e65722e90526567697374726174696f6e4e6f745065726d69747465644f6e526f6f745375626e6574002b04b84f7065726174696f6e206973206e6f74207065726d6974746564206f6e2074686520726f6f74207375626e65742e485374616b65546f6f4c6f77466f72526f6f74002c0415014120686f746b6579207769746820746f6f206c6974746c65207374616b6520697320617474656d7074696e6720746f206a6f696e2074686520726f6f74207375626e65742e54416c6c4e6574776f726b73496e496d6d756e697479002d049c416c6c207375626e6574732061726520696e2074686520696d6d756e69747920706572696f642e7c4e6f74456e6f75676842616c616e6365546f50617953776170486f744b6579002e04a84e6f7420656e6f7567682062616c616e636520746f20706179207377617070696e6720686f746b65792e344e6f74526f6f745375626e6574002f04dc4e657475696420646f6573206e6f74206d6174636820666f722073657474696e6720726f6f74206e6574776f726b20776569676874732e6c43616e4e6f74536574526f6f744e6574776f726b57656967687473003004a443616e206e6f7420736574207765696768747320666f722074686520726f6f74206e6574776f726b2e4c4e6f4e6575726f6e4964417661696c61626c65003104684e6f206e6575726f6e20494420697320617661696c61626c652e4844656c656761746554616b65546f6f4c6f770032046444656c65676174652074616b6520697320746f6f206c6f772e4c44656c656761746554616b65546f6f486967680033046844656c65676174652074616b6520697320746f6f20686967682e504e6f57656967687473436f6d6d6974466f756e6400340861014e6f20636f6d6d697420666f756e6420666f72207468652070726f766964656420686f746b65792b6e657475696420636f6d62696e6174696f6e207768656e20617474656d7074696e6720746f2072657665616c2074686520776569676874732e7c496e76616c696452657665616c436f6d6d6974486173684e6f744d61746368003504d4436f6d6d6974746564206861736820646f6573206e6f7420657175616c20746865206861736865642072657665616c20646174612e4c436f6d6d697452657665616c456e61626c6564003604f0417474656d7074696e6720746f2063616c6c207365745f77656967687473207768656e20636f6d6d69742f72657665616c20697320656e61626c656450436f6d6d697452657665616c44697361626c6564003704c8417474656d7470696e6720746f20636f6d6d69742f72657665616c2077656967687473207768656e2064697361626c65642e48436f756c644e6f744a6f696e53656e617465003804704e6f742061626c6520746f206a6f696e207468652073656e6174652e4c4c6971756964416c70686144697361626c6564003904bc417474656d7074696e6720746f2073657420616c70686120686967682f6c6f77207768696c652064697361626c65643c416c70686148696768546f6f4c6f77003a049c416c706861206869676820697320746f6f206c6f773a20616c7068615f68696768203e20302e3848416c7068614c6f774f75744f6652616e6765003b04ec416c706861206c6f77206973206f7574206f662072616e67653a20616c7068615f6c6f77203e203020262620616c7068615f6c6f77203c20302e3860436f6c644b6579416c72656164794173736f636961746564003c049054686520636f6c646b65792068617320616c7265616479206265656e2073776170706564804e6f74456e6f75676842616c616e6365546f50617953776170436f6c644b6579003d04d454686520636f6c646b65792062616c616e6365206973206e6f7420656e6f75676820746f2070617920666f7220746865207377617058436f6c646b65794973496e4172626974726174696f6e003e047454686520636f6c646b657920697320696e206172626974726174696f6e30496e76616c69644368696c64003f04f4417474656d7074696e6720746f2073657420616e20696e76616c6964206368696c6420666f72206120686f746b6579206f6e2061206e6574776f726b2e384475706c69636174654368696c64004004984475706c6963617465206368696c64207768656e2073657474696e67206368696c6472656e2e4850726f706f7274696f6e4f766572666c6f77004104a850726f706f7274696f6e206f766572666c6f77207768656e2073657474696e67206368696c6472656e2e3c546f6f4d616e794368696c6472656e00420460546f6f206d616e79206368696c6472656e204d415820352e4c5478526174654c696d69744578636565646564004304a044656661756c74207472616e73616374696f6e2072617465206c696d69742065786365656465642e5053776170416c72656164795363686564756c65640044045c5377617020616c7265616479207363686564756c65642e404661696c6564546f5363686564756c65004504586661696c656420746f207377617020636f6c646b6579484e6577436f6c644b65794973486f746b6579004604544e657720636f6c646b657920697320686f746b65794c496e76616c69644368696c646b657954616b65004704644368696c646b65792074616b6520697320696e76616c69642e7c54784368696c646b657954616b65526174654c696d69744578636565646564004804884368696c646b65792074616b652072617465206c696d69742065786365656465642e3c496e76616c69644964656e7469747900490444496e76616c6964206964656e746974792e544d656368616e69736d446f65734e6f744578697374004a040501547279696e6720746f2072656769737465722061207375626e657420696e746f2061206d656368616e69736d207468617420646f6573206e6f742065786973742e4443616e6e6f74556e7374616b654c6f636b004b048c547279696e6720746f20756e7374616b6520796f7572206c6f636b20616d6f756e742e3c5375626e65744e6f74457869737473004c04c0547279696e6720746f20706572666f726d20616374696f6e206f6e206e6f6e2d6578697374656e74207375626e65742e60546f6f4d616e79556e72657665616c6564436f6d6d697473004d04704d6178696d756d20636f6d6d6974206c696d697420726561636865644c45787069726564576569676874436f6d6d6974004e04b4417474656d7074656420746f2072657665616c207765696768747320746861742061726520657870697265642e3852657665616c546f6f4561726c79004f0498417474656d7074656420746f2072657665616c207765696768747320746f6f206561726c792e4c496e7075744c656e67746873556e657175616c0050041d01417474656d7074656420746f2062617463682072657665616c20776569676874732077697468206d69736d61746368656420766563746f7220696e707574206c656e676874732e60436f6d6d697474696e6757656967687473546f6f46617374005104e441207472616e736163746f72206578636565646564207468652072617465206c696d697420666f722073657474696e6720776569676874732e30416d6f756e74546f6f4c6f77005204605374616b6520616d6f756e7420697320746f6f206c6f772e54496e73756666696369656e744c6971756964697479005304544e6f7420656e6f756768206c69717569646974792e3c536c697070616765546f6f48696768005404a4536c69707061676520697320746f6f206869676820666f7220746865207472616e73616374696f6e2e485472616e73666572446973616c6c6f776564005504685375626e657420646973616c6c6f7773207472616e736665722e048054686520604572726f726020656e756d206f6620746869732070616c6c65742e11060c4c626f756e6465645f636f6c6c656374696f6e732c626f756e6465645f76656328426f756e6465645665630804540134045300000400b401185665633c543e00001506084470616c6c65745f636f6c6c65637469766514566f74657308244163636f756e74496401002c426c6f636b4e756d626572011000140114696e64657810013450726f706f73616c496e6465780001247468726573686f6c6410012c4d656d626572436f756e74000110617965735d0201385665633c4163636f756e7449643e0001106e6179735d0201385665633c4163636f756e7449643e00010c656e6410012c426c6f636b4e756d626572000019060c4470616c6c65745f636f6c6c6563746976651870616c6c6574144572726f72080454000449000128244e6f744d656d626572000004944163636f756e74206973206e6f742061206d656d626572206f6620636f6c6c656374697665444475706c696361746550726f706f73616c0001047c4475706c69636174652070726f706f73616c73206e6f7420616c6c6f7765644450726f706f73616c4e6f744578697374730002044c50726f706f73616c206d75737420657869737464496e6465784d69736d6174636850726f706f73616c4861736800030488496e646578206d69736d617463686564207468652070726f706f73616c2068617368344475706c6963617465566f7465000404584475706c696361746520766f74652069676e6f7265645c546f6f4561726c79546f436c6f736550726f706f73616c0005043d015468652063616c6c20746f20636c6f7365207468652070726f706f73616c20776173206d61646520746f6f206561726c792c206265666f72652074686520656e64206f662074686520766f74696e6758546f6f4d616e7941637469766550726f706f73616c73000604fc54686572652063616e206f6e6c792062652061206d6178696d756d206f6620604d617850726f706f73616c7360206163746976652070726f706f73616c732ea050726f706f73616c5765696768744c6573735468616e446973706174636843616c6c576569676874000704d054686520676976656e207765696768742d626f756e6420666f72207468652070726f706f73616c2077617320746f6f206c6f772ea450726f706f73616c4c656e677468426f756e644c6573735468616e50726f706f73616c4c656e677468000804d054686520676976656e206c656e6774682d626f756e6420666f72207468652070726f706f73616c2077617320746f6f206c6f772ea44475726174696f6e4c6f7765725468616e436f6e666967757265644d6f74696f6e4475726174696f6e000904dc54686520676976656e206d6f74696f6e206475726174696f6e20666f72207468652070726f706f73616c2077617320746f6f206c6f772e048054686520604572726f726020656e756d206f6620746869732070616c6c65742e1d060c4c626f756e6465645f636f6c6c656374696f6e732c626f756e6465645f76656328426f756e64656456656308045401000453000004005d0201185665633c543e000021060c4470616c6c65745f6d656d626572736869701870616c6c6574144572726f7208045400044900010c34416c72656164794d656d62657200000444416c72656164792061206d656d6265722e244e6f744d656d626572000104344e6f742061206d656d6265722e38546f6f4d616e794d656d6265727300020444546f6f206d616e79206d656d626572732e048054686520604572726f726020656e756d206f6620746869732070616c6c65742e25060c4c626f756e6465645f636f6c6c656374696f6e732c626f756e6465645f76656328426f756e64656456656308045401000453000004005d0201185665633c543e000029060c4470616c6c65745f6d656d626572736869701870616c6c6574144572726f7208045400044900010c34416c72656164794d656d62657200000444416c72656164792061206d656d6265722e244e6f744d656d626572000104344e6f742061206d656d6265722e38546f6f4d616e794d656d6265727300020444546f6f206d616e79206d656d626572732e048054686520604572726f726020656e756d206f6620746869732070616c6c65742e2d060c3870616c6c65745f7574696c6974791870616c6c6574144572726f7204045400010430546f6f4d616e7943616c6c730000045c546f6f206d616e792063616c6c7320626174636865642e048054686520604572726f726020656e756d206f6620746869732070616c6c65742e31060c2c70616c6c65745f7375646f1870616c6c6574144572726f720404540001042c526571756972655375646f0000048053656e646572206d75737420626520746865205375646f206163636f756e742e04684572726f7220666f7220746865205375646f2070616c6c65742e3506000004080004003906083c70616c6c65745f6d756c7469736967204d756c7469736967102c426c6f636b4e756d62657201101c42616c616e63650118244163636f756e7449640100304d6178417070726f76616c7300001001107768656ed8015854696d65706f696e743c426c6f636b4e756d6265723e00011c6465706f73697418011c42616c616e63650001246465706f7369746f720001244163636f756e744964000124617070726f76616c733d06018c426f756e6465645665633c4163636f756e7449642c204d6178417070726f76616c733e00003d060c4c626f756e6465645f636f6c6c656374696f6e732c626f756e6465645f76656328426f756e64656456656308045401000453000004005d0201185665633c543e000041060c3c70616c6c65745f6d756c74697369671870616c6c6574144572726f72040454000138404d696e696d756d5468726573686f6c640000047c5468726573686f6c64206d7573742062652032206f7220677265617465722e3c416c7265616479417070726f766564000104ac43616c6c20697320616c726561647920617070726f7665642062792074686973207369676e61746f72792e444e6f417070726f76616c734e65656465640002049c43616c6c20646f65736e2774206e65656420616e7920286d6f72652920617070726f76616c732e44546f6f4665775369676e61746f72696573000304a854686572652061726520746f6f20666577207369676e61746f7269657320696e20746865206c6973742e48546f6f4d616e795369676e61746f72696573000404ac54686572652061726520746f6f206d616e79207369676e61746f7269657320696e20746865206c6973742e545369676e61746f726965734f75744f664f726465720005040d01546865207369676e61746f7269657320776572652070726f7669646564206f7574206f66206f726465723b20746865792073686f756c64206265206f7264657265642e4c53656e646572496e5369676e61746f726965730006040d015468652073656e6465722077617320636f6e7461696e656420696e20746865206f74686572207369676e61746f726965733b2069742073686f756c646e27742062652e204e6f74466f756e64000704dc4d756c7469736967206f7065726174696f6e206e6f7420666f756e64207768656e20617474656d7074696e6720746f2063616e63656c2e204e6f744f776e65720008042d014f6e6c7920746865206163636f756e742074686174206f726967696e616c6c79206372656174656420746865206d756c74697369672069732061626c6520746f2063616e63656c2069742e2c4e6f54696d65706f696e740009041d014e6f2074696d65706f696e742077617320676976656e2c2079657420746865206d756c7469736967206f7065726174696f6e20697320616c726561647920756e6465727761792e3857726f6e6754696d65706f696e74000a042d014120646966666572656e742074696d65706f696e742077617320676976656e20746f20746865206d756c7469736967206f7065726174696f6e207468617420697320756e6465727761792e4c556e657870656374656454696d65706f696e74000b04f4412074696d65706f696e742077617320676976656e2c20796574206e6f206d756c7469736967206f7065726174696f6e20697320756e6465727761792e3c4d6178576569676874546f6f4c6f77000c04d0546865206d6178696d756d2077656967687420696e666f726d6174696f6e2070726f76696465642077617320746f6f206c6f772e34416c726561647953746f726564000d04a0546865206461746120746f2062652073746f72656420697320616c72656164792073746f7265642e048054686520604572726f726020656e756d206f6620746869732070616c6c65742e4506083c70616c6c65745f707265696d616765404f6c645265717565737453746174757308244163636f756e74496401001c42616c616e6365011801082c556e72657175657374656408011c6465706f73697449060150284163636f756e7449642c2042616c616e63652900010c6c656e10010c753332000000245265717565737465640c011c6465706f7369744d0601704f7074696f6e3c284163636f756e7449642c2042616c616e6365293e000114636f756e7410010c75333200010c6c656ed101012c4f7074696f6e3c7533323e000100004906000004080018004d0604184f7074696f6e0404540149060108104e6f6e6500000010536f6d650400490600000100005106083c70616c6c65745f707265696d616765345265717565737453746174757308244163636f756e7449640100185469636b657401550601082c556e7265717565737465640801187469636b65745906014c284163636f756e7449642c205469636b65742900010c6c656e10010c753332000000245265717565737465640c01306d617962655f7469636b65745d06016c4f7074696f6e3c284163636f756e7449642c205469636b6574293e000114636f756e7410010c7533320001246d617962655f6c656ed101012c4f7074696f6e3c7533323e00010000550614346672616d655f737570706f72741874726169747318746f6b656e732066756e6769626c6544486f6c64436f6e73696465726174696f6e1404410004460004520004440008467000000400180128463a3a42616c616e63650000590600000408005506005d0604184f7074696f6e0404540159060108104e6f6e6500000010536f6d6504005906000001000061060000040834100065060c4c626f756e6465645f636f6c6c656374696f6e732c626f756e6465645f76656328426f756e64656456656308045401080453000004003801185665633c543e000069060c3c70616c6c65745f707265696d6167651870616c6c6574144572726f7204045400012018546f6f426967000004a0507265696d61676520697320746f6f206c6172676520746f2073746f7265206f6e2d636861696e2e30416c72656164794e6f746564000104a4507265696d6167652068617320616c7265616479206265656e206e6f746564206f6e2d636861696e2e344e6f74417574686f72697a6564000204c85468652075736572206973206e6f7420617574686f72697a656420746f20706572666f726d207468697320616374696f6e2e204e6f744e6f746564000304fc54686520707265696d6167652063616e6e6f742062652072656d6f7665642073696e636520697420686173206e6f7420796574206265656e206e6f7465642e2452657175657374656400040409014120707265696d616765206d6179206e6f742062652072656d6f766564207768656e20746865726520617265206f75747374616e64696e672072657175657374732e304e6f745265717565737465640005042d0154686520707265696d61676520726571756573742063616e6e6f742062652072656d6f7665642073696e6365206e6f206f75747374616e64696e672072657175657374732065786973742e1c546f6f4d616e7900060455014d6f7265207468616e20604d41585f484153485f555047524144455f42554c4b5f434f554e54602068617368657320776572652072657175657374656420746f206265207570677261646564206174206f6e63652e18546f6f466577000704e4546f6f206665772068617368657320776572652072657175657374656420746f2062652075706772616465642028692e652e207a65726f292e048054686520604572726f726020656e756d206f6620746869732070616c6c65742e6d060c4c626f756e6465645f636f6c6c656374696f6e732c626f756e6465645f76656328426f756e646564566563080454017106045300000400850601185665633c543e0000710604184f7074696f6e0404540175060108104e6f6e6500000010536f6d650400750600000100007506084070616c6c65745f7363686564756c6572245363686564756c656414104e616d6501041043616c6c0179062c426c6f636b4e756d62657201103450616c6c6574734f726967696e013d03244163636f756e7449640100001401206d617962655f6964e801304f7074696f6e3c4e616d653e0001207072696f726974790801487363686564756c653a3a5072696f7269747900011063616c6c7906011043616c6c0001386d617962655f706572696f646963610301944f7074696f6e3c7363686564756c653a3a506572696f643c426c6f636b4e756d6265723e3e0001186f726967696e3d03013450616c6c6574734f726967696e0000790610346672616d655f737570706f72741874726169747324707265696d616765731c426f756e6465640804540125030448017d06010c184c656761637904011068617368340124483a3a4f757470757400000018496e6c696e65040081060134426f756e646564496e6c696e65000100184c6f6f6b757008011068617368340124483a3a4f757470757400010c6c656e10010c753332000200007d060c2873705f72756e74696d65187472616974732c426c616b6554776f3235360000000081060c4c626f756e6465645f636f6c6c656374696f6e732c626f756e6465645f76656328426f756e64656456656308045401080453000004003801185665633c543e000085060000027106008906084070616c6c65745f7363686564756c65722c5265747279436f6e6669670418506572696f640110000c0134746f74616c5f72657472696573080108753800012472656d61696e696e670801087538000118706572696f64100118506572696f6400008d060c4070616c6c65745f7363686564756c65721870616c6c6574144572726f72040454000114404661696c6564546f5363686564756c65000004644661696c656420746f207363686564756c6520612063616c6c204e6f74466f756e640001047c43616e6e6f742066696e6420746865207363686564756c65642063616c6c2e5c546172676574426c6f636b4e756d626572496e50617374000204a4476976656e2074617267657420626c6f636b206e756d62657220697320696e2074686520706173742e4852657363686564756c654e6f4368616e6765000304f052657363686564756c65206661696c6564206265636175736520697420646f6573206e6f74206368616e6765207363686564756c65642074696d652e144e616d6564000404d0417474656d707420746f207573652061206e6f6e2d6e616d65642066756e6374696f6e206f6e2061206e616d6564207461736b2e048054686520604572726f726020656e756d206f6620746869732070616c6c65742e9106000004089506180095060c4c626f756e6465645f636f6c6c656374696f6e732c626f756e6465645f76656328426f756e6465645665630804540199060453000004009d0601185665633c543e00009906083070616c6c65745f70726f78793c50726f7879446566696e6974696f6e0c244163636f756e74496401002450726f78795479706501f02c426c6f636b4e756d6265720110000c012064656c65676174650001244163636f756e74496400012870726f78795f74797065f0012450726f78795479706500011464656c617910012c426c6f636b4e756d62657200009d06000002990600a10600000408a5061800a5060c4c626f756e6465645f636f6c6c656374696f6e732c626f756e6465645f76656328426f756e64656456656308045401a906045300000400ad0601185665633c543e0000a906083070616c6c65745f70726f787930416e6e6f756e63656d656e740c244163636f756e7449640100104861736801342c426c6f636b4e756d6265720110000c01107265616c0001244163636f756e74496400012463616c6c5f686173683401104861736800011868656967687410012c426c6f636b4e756d6265720000ad06000002a90600b1060c3070616c6c65745f70726f78791870616c6c6574144572726f720404540001201c546f6f4d616e79000004210154686572652061726520746f6f206d616e792070726f786965732072656769737465726564206f7220746f6f206d616e7920616e6e6f756e63656d656e74732070656e64696e672e204e6f74466f756e640001047450726f787920726567697374726174696f6e206e6f7420666f756e642e204e6f7450726f7879000204cc53656e646572206973206e6f7420612070726f7879206f6620746865206163636f756e7420746f2062652070726f786965642e2c556e70726f787961626c650003042101412063616c6c20776869636820697320696e636f6d70617469626c652077697468207468652070726f7879207479706527732066696c7465722077617320617474656d707465642e244475706c69636174650004046c4163636f756e7420697320616c726561647920612070726f78792e304e6f5065726d697373696f6e000504150143616c6c206d6179206e6f74206265206d6164652062792070726f78792062656361757365206974206d617920657363616c617465206974732070726976696c656765732e2c556e616e6e6f756e636564000604d0416e6e6f756e63656d656e742c206966206d61646520617420616c6c2c20776173206d61646520746f6f20726563656e746c792e2c4e6f53656c6650726f78790007046443616e6e6f74206164642073656c662061732070726f78792e048054686520604572726f726020656e756d206f6620746869732070616c6c65742eb5060c3c70616c6c65745f726567697374727914747970657330526567697374726174696f6e081c42616c616e636501184c4d61784164646974696f6e616c4669656c6473000008011c6465706f73697418011c42616c616e6365000110696e666f710301844964656e74697479496e666f3c4d61784164646974696f6e616c4669656c64733e0000b9060c3c70616c6c65745f72656769737472791870616c6c6574144572726f7204045400010c3843616e6e6f74526567697374657200000435014163636f756e7420617474656d7074656420746f20726567697374657220616e206964656e746974792062757420646f6573206e6f74206d6565742074686520726571756972656d656e74732e6c546f6f4d616e794669656c6473496e4964656e74697479496e666f000104ec4163636f756e742070617373656420746f6f206d616e79206164646974696f6e616c206669656c647320746f207468656972206964656e74697479344e6f7452656769737465726564000204a84163636f756e7420646f65736e2774206861766520612072656769737465726564206964656e74697479048054686520604572726f726020656e756d206f6620746869732070616c6c65742ebd060c4870616c6c65745f636f6d6d69746d656e747314747970657330526567697374726174696f6e0c1c42616c616e63650118244d61784669656c6473002c426c6f636b4e756d6265720110000c011c6465706f73697418011c42616c616e6365000114626c6f636b10012c426c6f636b4e756d626572000110696e666f7d040164436f6d6d69746d656e74496e666f3c4d61784669656c64733e0000c1060c4870616c6c65745f636f6d6d69746d656e74731870616c6c6574144572726f7204045400010c74546f6f4d616e794669656c6473496e436f6d6d69746d656e74496e666f000004f44163636f756e742070617373656420746f6f206d616e79206164646974696f6e616c206669656c647320746f20746865697220636f6d6d69746d656e745c4163636f756e744e6f74416c6c6f776564436f6d6d6974000104d44163636f756e74206973206e6f7420616c6c6f7720746f206d616b6520636f6d6d69746d656e747320746f2074686520636861696e78436f6d6d69746d656e74536574526174654c696d69744578636565646564000204f84163636f756e7420697320747279696e6720746f20636f6d6d6974206461746120746f6f20666173742c2072617465206c696d6974206578636565646564048054686520604572726f726020656e756d206f6620746869732070616c6c65742ec5060c4870616c6c65745f61646d696e5f7574696c731870616c6c6574144572726f7204045400010c485375626e6574446f65734e6f744578697374000004d4546865207375626e657420646f6573206e6f742065786973742c20636865636b20746865206e657475696420706172616d65746572784d617856616c696461746f72734c61726765725468616e4d617855496473000104ad01546865206d6178696d756d206e756d626572206f66207375626e65742076616c696461746f7273206d757374206265206c657373207468616e20746865206d6178696d756d206e756d626572206f6620616c6c6f776564205549447320696e20746865207375626e65742e844d6178416c6c6f776564554964734c6573735468616e43757272656e7455496473000204ad01546865206d6178696d756d206e756d626572206f66207375626e65742076616c696461746f7273206d757374206265206d6f7265207468616e207468652063757272656e74206e756d626572206f66205549447320616c726561647920696e20746865207375626e65742e048054686520604572726f726020656e756d206f6620746869732070616c6c65742ec90600000408001000cd0604184f7074696f6e04045401180108104e6f6e6500000010536f6d650400180000010000d1060c4070616c6c65745f736166655f6d6f64651870616c6c6574144572726f7204045400011c1c456e7465726564000004b054686520736166652d6d6f64652069732028616c7265616479206f72207374696c6c2920656e74657265642e18457869746564000104ac54686520736166652d6d6f64652069732028616c7265616479206f72207374696c6c29206578697465642e344e6f74436f6e666967757265640002040901546869732066756e6374696f6e616c697479206f66207468652070616c6c65742069732064697361626c65642062792074686520636f6e66696775726174696f6e2e244e6f4465706f736974000304745468657265206973206e6f2062616c616e63652072657365727665642e40416c72656164794465706f73697465640004045d01546865206163636f756e7420616c7265616479206861732061206465706f73697420726573657276656420616e642063616e207468657265666f7265206e6f7420656e746572206f7220657874656e6420616761696e2e4043616e6e6f7452656c656173655965740005049054686973206465706f7369742063616e6e6f742062652072656c6561736564207965742e3443757272656e63794572726f72000604a0416e206572726f722066726f6d2074686520756e6465726c79696e67206043757272656e6379602e048054686520604572726f726020656e756d206f6620746869732070616c6c65742ed506000002d90600d9060000040c9905dd06f10600dd06081866705f727063445472616e73616374696f6e53746174757300001c01407472616e73616374696f6e5f68617368340110483235360001447472616e73616374696f6e5f696e64657810010c75333200011066726f6d0d01011c41646472657373000108746fe106013c4f7074696f6e3c416464726573733e000140636f6e74726163745f61646472657373e106013c4f7074696f6e3c416464726573733e0001106c6f6773e50601205665633c4c6f673e0001286c6f67735f626c6f6f6de9060114426c6f6f6d0000e10604184f7074696f6e040454010d010108104e6f6e6500000010536f6d6504000d010000010000e506000002390100e9060820657468626c6f6f6d14426c6f6f6d00000400ed0601405b75383b20424c4f4f4d5f53495a455d0000ed06000003000100000800f1060c20657468657265756d1c726563656970742452656365697074563300010c184c65676163790400f506014445495036353852656365697074446174610000001c454950323933300400f50601484549503239333052656365697074446174610001001c454950313535390400f506014845495031353539526563656970744461746100020000f5060c20657468657265756d1c72656365697074444549503635385265636569707444617461000010012c7374617475735f636f64650801087538000120757365645f67617341010110553235360001286c6f67735f626c6f6f6de9060114426c6f6f6d0001106c6f6773e50601205665633c4c6f673e0000f9060c20657468657265756d14626c6f636b14426c6f636b040454019905000c0118686561646572fd0601184865616465720001307472616e73616374696f6e73050701185665633c543e0001186f6d6d6572730907012c5665633c4865616465723e0000fd060c20657468657265756d186865616465721848656164657200003c012c706172656e745f686173683401104832353600012c6f6d6d6572735f686173683401104832353600012c62656e65666963696172790d0101104831363000012873746174655f726f6f74340110483235360001447472616e73616374696f6e735f726f6f743401104832353600013472656365697074735f726f6f74340110483235360001286c6f67735f626c6f6f6de9060114426c6f6f6d000128646966666963756c747941010110553235360001186e756d62657241010110553235360001246761735f6c696d697441010110553235360001206761735f75736564410101105532353600012474696d657374616d7018010c75363400012865787472615f6461746138011442797465730001206d69785f68617368340110483235360001146e6f6e63650107010c483634000001070c38657468657265756d5f747970657310686173680c48363400000400a501011c5b75383b20385d000005070000029905000907000002fd06000d07000002f106001107000002dd060015070c3c70616c6c65745f657468657265756d1870616c6c6574144572726f7204045400010840496e76616c69645369676e6174757265000004545369676e617475726520697320696e76616c69642e305072654c6f67457869737473000104d85072652d6c6f672069732070726573656e742c207468657265666f7265207472616e73616374206973206e6f7420616c6c6f7765642e048054686520604572726f726020656e756d206f6620746869732070616c6c65742e1907082870616c6c65745f65766d30436f64654d65746164617461000008011073697a6518010c753634000110686173683401104832353600001d07000004080d01340021070c2870616c6c65745f65766d1870616c6c6574144572726f720404540001382842616c616e63654c6f77000004904e6f7420656e6f7567682062616c616e636520746f20706572666f726d20616374696f6e2c4665654f766572666c6f770001048043616c63756c6174696e6720746f74616c20666565206f766572666c6f7765643c5061796d656e744f766572666c6f770002049043616c63756c6174696e6720746f74616c207061796d656e74206f766572666c6f7765643857697468647261774661696c65640003044c576974686472617720666565206661696c6564384761735072696365546f6f4c6f770004045447617320707269636520697320746f6f206c6f772e30496e76616c69644e6f6e6365000504404e6f6e636520697320696e76616c6964384761734c696d6974546f6f4c6f7700060454476173206c696d697420697320746f6f206c6f772e3c4761734c696d6974546f6f4869676800070458476173206c696d697420697320746f6f20686967682e38496e76616c6964436861696e49640008046054686520636861696e20696420697320696e76616c69642e40496e76616c69645369676e617475726500090464746865207369676e617475726520697320696e76616c69642e285265656e7472616e6379000a043845564d207265656e7472616e6379685472616e73616374696f6e4d757374436f6d6546726f6d454f41000b04244549502d333630372c24556e646566696e6564000c0440556e646566696e6564206572726f722e284e6f74416c6c6f776564000d04bc4f726967696e206973206e6f7420616c6c6f77656420746f20706572666f726d20746865206f7065726174696f6e2e048054686520604572726f726020656e756d206f6620746869732070616c6c65742e25070c3070616c6c65745f6472616e641870616c6c6574144572726f72040454000118244e6f6e6556616c7565000004f85468652076616c7565207265747269657665642077617320604e6f6e6560206173206e6f2076616c7565207761732070726576696f75736c79207365742e3c53746f726167654f766572666c6f770001041d0154686572652077617320616e20617474656d707420746f20696e6372656d656e74207468652076616c756520696e2073746f72616765206f76657220607533323a3a4d4158602e584472616e64436f6e6e656374696f6e4661696c757265000204606661696c656420746f20636f6e6e65637420746f207468653c556e766572696669656450756c7365000304507468652070756c736520697320696e76616c696448496e76616c6964526f756e644e756d6265720004048874686520726f756e64206e756d62657220646964206e6f7420696e6372656d656e745850756c7365566572696669636174696f6e4572726f720005047c7468652070756c736520636f756c64206e6f74206265207665726966696564048054686520604572726f726020656e756d206f6620746869732070616c6c65742e29070000042c2d073107350739073d07450749074d07510759075d07002d0710306672616d655f73797374656d28657874656e73696f6e7354636865636b5f6e6f6e5f7a65726f5f73656e64657248436865636b4e6f6e5a65726f53656e64657204045400000000310710306672616d655f73797374656d28657874656e73696f6e7348636865636b5f737065635f76657273696f6e40436865636b5370656356657273696f6e04045400000000350710306672616d655f73797374656d28657874656e73696f6e7340636865636b5f74785f76657273696f6e38436865636b547856657273696f6e04045400000000390710306672616d655f73797374656d28657874656e73696f6e7334636865636b5f67656e6573697330436865636b47656e65736973040454000000003d0710306672616d655f73797374656d28657874656e73696f6e733c636865636b5f6d6f7274616c69747938436865636b4d6f7274616c697479040454000004004107010c45726100004107102873705f72756e74696d651c67656e657269630c6572610c4572610001010420496d6d6f7274616c0000001c4d6f7274616c31040008000001001c4d6f7274616c32040008000002001c4d6f7274616c33040008000003001c4d6f7274616c34040008000004001c4d6f7274616c35040008000005001c4d6f7274616c36040008000006001c4d6f7274616c37040008000007001c4d6f7274616c38040008000008001c4d6f7274616c3904000800000900204d6f7274616c313004000800000a00204d6f7274616c313104000800000b00204d6f7274616c313204000800000c00204d6f7274616c313304000800000d00204d6f7274616c313404000800000e00204d6f7274616c313504000800000f00204d6f7274616c313604000800001000204d6f7274616c313704000800001100204d6f7274616c313804000800001200204d6f7274616c313904000800001300204d6f7274616c323004000800001400204d6f7274616c323104000800001500204d6f7274616c323204000800001600204d6f7274616c323304000800001700204d6f7274616c323404000800001800204d6f7274616c323504000800001900204d6f7274616c323604000800001a00204d6f7274616c323704000800001b00204d6f7274616c323804000800001c00204d6f7274616c323904000800001d00204d6f7274616c333004000800001e00204d6f7274616c333104000800001f00204d6f7274616c333204000800002000204d6f7274616c333304000800002100204d6f7274616c333404000800002200204d6f7274616c333504000800002300204d6f7274616c333604000800002400204d6f7274616c333704000800002500204d6f7274616c333804000800002600204d6f7274616c333904000800002700204d6f7274616c343004000800002800204d6f7274616c343104000800002900204d6f7274616c343204000800002a00204d6f7274616c343304000800002b00204d6f7274616c343404000800002c00204d6f7274616c343504000800002d00204d6f7274616c343604000800002e00204d6f7274616c343704000800002f00204d6f7274616c343804000800003000204d6f7274616c343904000800003100204d6f7274616c353004000800003200204d6f7274616c353104000800003300204d6f7274616c353204000800003400204d6f7274616c353304000800003500204d6f7274616c353404000800003600204d6f7274616c353504000800003700204d6f7274616c353604000800003800204d6f7274616c353704000800003900204d6f7274616c353804000800003a00204d6f7274616c353904000800003b00204d6f7274616c363004000800003c00204d6f7274616c363104000800003d00204d6f7274616c363204000800003e00204d6f7274616c363304000800003f00204d6f7274616c363404000800004000204d6f7274616c363504000800004100204d6f7274616c363604000800004200204d6f7274616c363704000800004300204d6f7274616c363804000800004400204d6f7274616c363904000800004500204d6f7274616c373004000800004600204d6f7274616c373104000800004700204d6f7274616c373204000800004800204d6f7274616c373304000800004900204d6f7274616c373404000800004a00204d6f7274616c373504000800004b00204d6f7274616c373604000800004c00204d6f7274616c373704000800004d00204d6f7274616c373804000800004e00204d6f7274616c373904000800004f00204d6f7274616c383004000800005000204d6f7274616c383104000800005100204d6f7274616c383204000800005200204d6f7274616c383304000800005300204d6f7274616c383404000800005400204d6f7274616c383504000800005500204d6f7274616c383604000800005600204d6f7274616c383704000800005700204d6f7274616c383804000800005800204d6f7274616c383904000800005900204d6f7274616c393004000800005a00204d6f7274616c393104000800005b00204d6f7274616c393204000800005c00204d6f7274616c393304000800005d00204d6f7274616c393404000800005e00204d6f7274616c393504000800005f00204d6f7274616c393604000800006000204d6f7274616c393704000800006100204d6f7274616c393804000800006200204d6f7274616c393904000800006300244d6f7274616c31303004000800006400244d6f7274616c31303104000800006500244d6f7274616c31303204000800006600244d6f7274616c31303304000800006700244d6f7274616c31303404000800006800244d6f7274616c31303504000800006900244d6f7274616c31303604000800006a00244d6f7274616c31303704000800006b00244d6f7274616c31303804000800006c00244d6f7274616c31303904000800006d00244d6f7274616c31313004000800006e00244d6f7274616c31313104000800006f00244d6f7274616c31313204000800007000244d6f7274616c31313304000800007100244d6f7274616c31313404000800007200244d6f7274616c31313504000800007300244d6f7274616c31313604000800007400244d6f7274616c31313704000800007500244d6f7274616c31313804000800007600244d6f7274616c31313904000800007700244d6f7274616c31323004000800007800244d6f7274616c31323104000800007900244d6f7274616c31323204000800007a00244d6f7274616c31323304000800007b00244d6f7274616c31323404000800007c00244d6f7274616c31323504000800007d00244d6f7274616c31323604000800007e00244d6f7274616c31323704000800007f00244d6f7274616c31323804000800008000244d6f7274616c31323904000800008100244d6f7274616c31333004000800008200244d6f7274616c31333104000800008300244d6f7274616c31333204000800008400244d6f7274616c31333304000800008500244d6f7274616c31333404000800008600244d6f7274616c31333504000800008700244d6f7274616c31333604000800008800244d6f7274616c31333704000800008900244d6f7274616c31333804000800008a00244d6f7274616c31333904000800008b00244d6f7274616c31343004000800008c00244d6f7274616c31343104000800008d00244d6f7274616c31343204000800008e00244d6f7274616c31343304000800008f00244d6f7274616c31343404000800009000244d6f7274616c31343504000800009100244d6f7274616c31343604000800009200244d6f7274616c31343704000800009300244d6f7274616c31343804000800009400244d6f7274616c31343904000800009500244d6f7274616c31353004000800009600244d6f7274616c31353104000800009700244d6f7274616c31353204000800009800244d6f7274616c31353304000800009900244d6f7274616c31353404000800009a00244d6f7274616c31353504000800009b00244d6f7274616c31353604000800009c00244d6f7274616c31353704000800009d00244d6f7274616c31353804000800009e00244d6f7274616c31353904000800009f00244d6f7274616c3136300400080000a000244d6f7274616c3136310400080000a100244d6f7274616c3136320400080000a200244d6f7274616c3136330400080000a300244d6f7274616c3136340400080000a400244d6f7274616c3136350400080000a500244d6f7274616c3136360400080000a600244d6f7274616c3136370400080000a700244d6f7274616c3136380400080000a800244d6f7274616c3136390400080000a900244d6f7274616c3137300400080000aa00244d6f7274616c3137310400080000ab00244d6f7274616c3137320400080000ac00244d6f7274616c3137330400080000ad00244d6f7274616c3137340400080000ae00244d6f7274616c3137350400080000af00244d6f7274616c3137360400080000b000244d6f7274616c3137370400080000b100244d6f7274616c3137380400080000b200244d6f7274616c3137390400080000b300244d6f7274616c3138300400080000b400244d6f7274616c3138310400080000b500244d6f7274616c3138320400080000b600244d6f7274616c3138330400080000b700244d6f7274616c3138340400080000b800244d6f7274616c3138350400080000b900244d6f7274616c3138360400080000ba00244d6f7274616c3138370400080000bb00244d6f7274616c3138380400080000bc00244d6f7274616c3138390400080000bd00244d6f7274616c3139300400080000be00244d6f7274616c3139310400080000bf00244d6f7274616c3139320400080000c000244d6f7274616c3139330400080000c100244d6f7274616c3139340400080000c200244d6f7274616c3139350400080000c300244d6f7274616c3139360400080000c400244d6f7274616c3139370400080000c500244d6f7274616c3139380400080000c600244d6f7274616c3139390400080000c700244d6f7274616c3230300400080000c800244d6f7274616c3230310400080000c900244d6f7274616c3230320400080000ca00244d6f7274616c3230330400080000cb00244d6f7274616c3230340400080000cc00244d6f7274616c3230350400080000cd00244d6f7274616c3230360400080000ce00244d6f7274616c3230370400080000cf00244d6f7274616c3230380400080000d000244d6f7274616c3230390400080000d100244d6f7274616c3231300400080000d200244d6f7274616c3231310400080000d300244d6f7274616c3231320400080000d400244d6f7274616c3231330400080000d500244d6f7274616c3231340400080000d600244d6f7274616c3231350400080000d700244d6f7274616c3231360400080000d800244d6f7274616c3231370400080000d900244d6f7274616c3231380400080000da00244d6f7274616c3231390400080000db00244d6f7274616c3232300400080000dc00244d6f7274616c3232310400080000dd00244d6f7274616c3232320400080000de00244d6f7274616c3232330400080000df00244d6f7274616c3232340400080000e000244d6f7274616c3232350400080000e100244d6f7274616c3232360400080000e200244d6f7274616c3232370400080000e300244d6f7274616c3232380400080000e400244d6f7274616c3232390400080000e500244d6f7274616c3233300400080000e600244d6f7274616c3233310400080000e700244d6f7274616c3233320400080000e800244d6f7274616c3233330400080000e900244d6f7274616c3233340400080000ea00244d6f7274616c3233350400080000eb00244d6f7274616c3233360400080000ec00244d6f7274616c3233370400080000ed00244d6f7274616c3233380400080000ee00244d6f7274616c3233390400080000ef00244d6f7274616c3234300400080000f000244d6f7274616c3234310400080000f100244d6f7274616c3234320400080000f200244d6f7274616c3234330400080000f300244d6f7274616c3234340400080000f400244d6f7274616c3234350400080000f500244d6f7274616c3234360400080000f600244d6f7274616c3234370400080000f700244d6f7274616c3234380400080000f800244d6f7274616c3234390400080000f900244d6f7274616c3235300400080000fa00244d6f7274616c3235310400080000fb00244d6f7274616c3235320400080000fc00244d6f7274616c3235330400080000fd00244d6f7274616c3235340400080000fe00244d6f7274616c3235350400080000ff000045070c586e6f64655f73756274656e736f725f72756e74696d652c636865636b5f6e6f6e636528436865636b4e6f6e63650404540000040061010120543a3a4e6f6e63650000490710306672616d655f73797374656d28657874656e73696f6e7330636865636b5f7765696768742c436865636b576569676874040454000000004d07086870616c6c65745f7472616e73616374696f6e5f7061796d656e74604368617267655472616e73616374696f6e5061796d656e740404540000040030013042616c616e63654f663c543e00005107084070616c6c65745f73756274656e736f726053756274656e736f725369676e6564457874656e73696f6e040454015507000000550708586e6f64655f73756274656e736f725f72756e74696d651c52756e74696d65000000005907084870616c6c65745f636f6d6d69746d656e747368436f6d6d69746d656e74735369676e6564457874656e73696f6e0404540155070000005d0708746672616d655f6d657461646174615f686173685f657874656e73696f6e44436865636b4d657461646174614861736804045400000401106d6f6465610701104d6f64650000610708746672616d655f6d657461646174615f686173685f657874656e73696f6e104d6f64650001082044697361626c65640000001c456e61626c6564000100006507102873705f72756e74696d651c67656e6572696314626c6f636b14426c6f636b08184865616465720169072445787472696e736963016d07000801186865616465726907011848656164657200012865787472696e73696373750701385665633c45787472696e7369633e00006907102873705f72756e74696d651c67656e65726963186865616465721848656164657208184e756d62657201101048617368000014012c706172656e745f68617368340130486173683a3a4f75747075740001186e756d626572610101184e756d62657200012873746174655f726f6f74340130486173683a3a4f757470757400013c65787472696e736963735f726f6f74340130486173683a3a4f75747075740001186469676573743c011844696765737400006d070c4466705f73656c665f636f6e7461696e65644c756e636865636b65645f65787472696e73696348556e636865636b656445787472696e736963101c416464726573730155021043616c6c012503245369676e617475726501f505144578747261012907000400710701250173705f72756e74696d653a3a67656e657269633a3a556e636865636b656445787472696e7369633c416464726573732c2043616c6c2c205369676e61747572652c2045787472610a3e00007107102873705f72756e74696d651c67656e657269634c756e636865636b65645f65787472696e73696348556e636865636b656445787472696e736963101c416464726573730155021043616c6c012503245369676e617475726501f5051445787472610129070004003800000075070000026d07007907082873705f72756e74696d655845787472696e736963496e636c7573696f6e4d6f646500010834416c6c45787472696e73696373000000344f6e6c79496e686572656e7473000100007d07081c73705f636f7265384f70617175654d657461646174610000040038011c5665633c75383e0000810704184f7074696f6e040454017d070108104e6f6e6500000010536f6d6504007d0700000100008507000002100089070418526573756c7408045401a00445018d070108084f6b0400a0000000000c45727204008d0700000100008d070c2873705f72756e74696d65507472616e73616374696f6e5f76616c6964697479605472616e73616374696f6e56616c69646974794572726f720001081c496e76616c6964040091070148496e76616c69645472616e73616374696f6e0000001c556e6b6e6f776e040095070148556e6b6e6f776e5472616e73616374696f6e0001000091070c2873705f72756e74696d65507472616e73616374696f6e5f76616c696469747948496e76616c69645472616e73616374696f6e00012c1043616c6c0000001c5061796d656e7400010018467574757265000200145374616c650003002042616450726f6f6600040044416e6369656e744269727468426c6f636b0005004445786861757374735265736f757263657300060018437573746f6d04000801087538000700304261644d616e6461746f72790008004c4d616e6461746f727956616c69646174696f6e000900244261645369676e6572000a000095070c2873705f72756e74696d65507472616e73616374696f6e5f76616c696469747948556e6b6e6f776e5472616e73616374696f6e00010c3043616e6e6f744c6f6f6b75700000004c4e6f556e7369676e656456616c696461746f7200010018437573746f6d04000801087538000200009907083073705f696e686572656e747330496e686572656e74446174610000040110646174619d07019442547265654d61703c496e686572656e744964656e7469666965722c205665633c75383e3e00009d07042042547265654d617008044b01a50104560138000400a107000000a107000002a50700a50700000408a5013800a907083073705f696e686572656e747350436865636b496e686572656e7473526573756c7400000c01106f6b6179240110626f6f6c00012c666174616c5f6572726f72240110626f6f6c0001186572726f727399070130496e686572656e74446174610000ad070418526573756c7408045401a404450129010108084f6b0400a4000000000c457272040029010000010000b10704184f7074696f6e0404540129010108104e6f6e6500000010536f6d65040029010000010000b50704184f7074696f6e04045401380108104e6f6e6500000010536f6d650400380000010000b907000002290100bd070c2873705f72756e74696d65507472616e73616374696f6e5f76616c6964697479445472616e73616374696f6e536f7572636500010c1c496e426c6f636b000000144c6f63616c0001002045787465726e616c00020000c1070418526573756c7408045401c5070445018d070108084f6b0400c507000000000c45727204008d070000010000c5070c2873705f72756e74696d65507472616e73616374696f6e5f76616c69646974794056616c69645472616e73616374696f6e00001401207072696f7269747918014c5472616e73616374696f6e5072696f7269747900012072657175697265737501014c5665633c5472616e73616374696f6e5461673e00012070726f76696465737501014c5665633c5472616e73616374696f6e5461673e0001246c6f6e6765766974791801505472616e73616374696f6e4c6f6e67657669747900012470726f706167617465240110626f6f6c0000c907084873705f636f6e73656e7375735f736c6f747330536c6f744475726174696f6e0000040018010c7536340000cd0704184f7074696f6e04045401d1070108104e6f6e6500000010536f6d650400d1070000010000d107000002d50700d5070000040838d90700d9070c1c73705f636f72651863727970746f244b65795479706549640000040048011c5b75383b20345d0000dd07082873705f72756e74696d652c4f706171756556616c75650000040038011c5665633c75383e0000e10704184f7074696f6e04045401a40108104e6f6e6500000010536f6d650400a40000010000e50704184f7074696f6e04045401dd070108104e6f6e6500000010536f6d650400dd070000010000e9070c6870616c6c65745f7472616e73616374696f6e5f7061796d656e741474797065734c52756e74696d654469737061746368496e666f081c42616c616e6365011818576569676874012c000c01187765696768742c0118576569676874000114636c6173736001344469737061746368436c61737300012c7061727469616c5f66656518011c42616c616e63650000ed070c6870616c6c65745f7472616e73616374696f6e5f7061796d656e741474797065732846656544657461696c73041c42616c616e6365011800080134696e636c7573696f6e5f666565f10701744f7074696f6e3c496e636c7573696f6e4665653c42616c616e63653e3e00010c74697018011c42616c616e63650000f10704184f7074696f6e04045401f5070108104e6f6e6500000010536f6d650400f5070000010000f5070c6870616c6c65745f7472616e73616374696f6e5f7061796d656e7414747970657330496e636c7573696f6e466565041c42616c616e63650118000c0120626173655f66656518011c42616c616e636500011c6c656e5f66656518011c42616c616e636500014c61646a75737465645f7765696768745f66656518011c42616c616e63650000f9070c0c65766d1c6261636b656e64144261736963000008011c62616c616e636541010110553235360001146e6f6e636541010110553235360000fd0704184f7074696f6e04045401c5050108104e6f6e6500000010536f6d650400c505000001000001080418526573756c74080454010508044501680108084f6b04000508000000000c45727204006800000100000508081866705f65766d3c457865637574696f6e496e666f563204045401380014012c657869745f726561736f6e1501012845786974526561736f6e00011476616c756538010454000120757365645f6761730908011c5573656447617300012c7765696768745f696e666f0d0801484f7074696f6e3c576569676874496e666f3e0001106c6f6773e50601205665633c4c6f673e00000908081866705f65766d1c5573656447617300000801207374616e646172644101011055323536000124656666656374697665410101105532353600000d0804184f7074696f6e0404540111080108104e6f6e6500000010536f6d650400110800000100001108081866705f65766d28576569676874496e666f00001001387265665f74696d655f6c696d6974cd06012c4f7074696f6e3c7536343e00014070726f6f665f73697a655f6c696d6974cd06012c4f7074696f6e3c7536343e0001387265665f74696d655f7573616765cd06012c4f7074696f6e3c7536343e00014070726f6f665f73697a655f7573616765cd06012c4f7074696f6e3c7536343e000015080418526573756c74080454011908044501680108084f6b04001908000000000c45727204006800000100001908081866705f65766d3c457865637574696f6e496e666f5632040454010d010014012c657869745f726561736f6e1501012845786974526561736f6e00011476616c75650d01010454000120757365645f6761730908011c5573656447617300012c7765696768745f696e666f0d0801484f7074696f6e3c576569676874496e666f3e0001106c6f6773e50601205665633c4c6f673e00001d0804184f7074696f6e04045401f9060108104e6f6e6500000010536f6d650400f9060000010000210804184f7074696f6e040454010d070108104e6f6e6500000010536f6d6504000d070000010000250804184f7074696f6e0404540111070108104e6f6e6500000010536f6d6504001107000001000029080000040c1d0821082508002d0804184f7074696f6e0404540149010108104e6f6e6500000010536f6d650400490100000100003108000004081d0825080035080000023908003908104070616c6c65745f73756274656e736f72207270635f696e666f3464656c65676174655f696e666f3044656c6567617465496e666f04244163636f756e74496401000020013464656c65676174655f737335380001244163636f756e74496400011074616b65bc0130436f6d706163743c7531363e0001286e6f6d696e61746f72733d0801785665633c284163636f756e7449642c20436f6d706163743c7536343e293e0001286f776e65725f737335380001244163636f756e744964000134726567697374726174696f6e73b801445665633c436f6d706163743c7531363e3e00014476616c696461746f725f7065726d697473b801445665633c436f6d706163743c7531363e3e00013c72657475726e5f7065725f31303030300130436f6d706163743c7536343e000148746f74616c5f6461696c795f72657475726e300130436f6d706163743c7536343e00003d08000002410800410800000408003000450804184f7074696f6e0404540139080108104e6f6e6500000010536f6d6504003908000001000049080000024d08004d08000004083908300051080000025508005508104070616c6c65745f73756274656e736f72207270635f696e666f2c6e6575726f6e5f696e666f284e6575726f6e496e666f04244163636f756e744964010000500118686f746b65790001244163636f756e74496400011c636f6c646b65790001244163636f756e74496400010c756964bc0130436f6d706163743c7531363e0001186e6574756964bc0130436f6d706163743c7531363e000118616374697665240110626f6f6c00012461786f6e5f696e666fd102012041786f6e496e666f00013c70726f6d6574686575735f696e666fdd02013850726f6d657468657573496e666f0001147374616b653d0801785665633c284163636f756e7449642c20436f6d706163743c7536343e293e00011072616e6bbc0130436f6d706163743c7531363e000120656d697373696f6e300130436f6d706163743c7536343e000124696e63656e74697665bc0130436f6d706163743c7531363e000124636f6e73656e737573bc0130436f6d706163743c7531363e0001147472757374bc0130436f6d706163743c7531363e00013c76616c696461746f725f7472757374bc0130436f6d706163743c7531363e0001246469766964656e6473bc0130436f6d706163743c7531363e00012c6c6173745f757064617465300130436f6d706163743c7536343e00014076616c696461746f725f7065726d6974240110626f6f6c00011c77656967687473150301845665633c28436f6d706163743c7531363e2c20436f6d706163743c7531363e293e000114626f6e6473150301845665633c28436f6d706163743c7531363e2c20436f6d706163743c7531363e293e0001347072756e696e675f73636f7265bc0130436f6d706163743c7531363e0000590804184f7074696f6e0404540155080108104e6f6e6500000010536f6d650400550800000100005d080000026108006108104070616c6c65745f73756274656e736f72207270635f696e666f2c6e6575726f6e5f696e666f384e6575726f6e496e666f4c69746504244163636f756e744964010000480118686f746b65790001244163636f756e74496400011c636f6c646b65790001244163636f756e74496400010c756964bc0130436f6d706163743c7531363e0001186e6574756964bc0130436f6d706163743c7531363e000118616374697665240110626f6f6c00012461786f6e5f696e666fd102012041786f6e496e666f00013c70726f6d6574686575735f696e666fdd02013850726f6d657468657573496e666f0001147374616b653d0801785665633c284163636f756e7449642c20436f6d706163743c7536343e293e00011072616e6bbc0130436f6d706163743c7531363e000120656d697373696f6e300130436f6d706163743c7536343e000124696e63656e74697665bc0130436f6d706163743c7531363e000124636f6e73656e737573bc0130436f6d706163743c7531363e0001147472757374bc0130436f6d706163743c7531363e00013c76616c696461746f725f7472757374bc0130436f6d706163743c7531363e0001246469766964656e6473bc0130436f6d706163743c7531363e00012c6c6173745f757064617465300130436f6d706163743c7536343e00014076616c696461746f725f7065726d6974240110626f6f6c0001347072756e696e675f73636f7265bc0130436f6d706163743c7531363e0000650804184f7074696f6e0404540161080108104e6f6e6500000010536f6d65040061080000010000690804184f7074696f6e040454016d080108104e6f6e6500000010536f6d6504006d0800000100006d08104070616c6c65745f73756274656e736f72207270635f696e666f2c7375626e65745f696e666f285375626e6574496e666f04244163636f756e7449640100004801186e6574756964bc0130436f6d706163743c7531363e00010c72686fbc0130436f6d706163743c7531363e0001146b61707061bc0130436f6d706163743c7531363e000128646966666963756c7479300130436f6d706163743c7536343e00013c696d6d756e6974795f706572696f64bc0130436f6d706163743c7531363e0001586d61785f616c6c6f7765645f76616c696461746f7273bc0130436f6d706163743c7531363e00014c6d696e5f616c6c6f7765645f77656967687473bc0130436f6d706163743c7531363e0001446d61785f776569676874735f6c696d6974bc0130436f6d706163743c7531363e0001447363616c696e675f6c61775f706f776572bc0130436f6d706163743c7531363e0001307375626e6574776f726b5f6ebc0130436f6d706163743c7531363e0001406d61785f616c6c6f7765645f75696473bc0130436f6d706163743c7531363e000158626c6f636b735f73696e63655f6c6173745f73746570300130436f6d706163743c7536343e00011474656d706fbc0130436f6d706163743c7531363e0001406e6574776f726b5f6d6f64616c697479bc0130436f6d706163743c7531363e00013c6e6574776f726b5f636f6e6e656374710801345665633c5b7531363b20325d3e00013c656d697373696f6e5f76616c756573300130436f6d706163743c7536343e0001106275726e300130436f6d706163743c7536343e0001146f776e65720001244163636f756e744964000071080000027508007508000003020000009c0079080000026908007d0804184f7074696f6e0404540181080108104e6f6e6500000010536f6d650400810800000100008108104070616c6c65745f73756274656e736f72207270635f696e666f2c7375626e65745f696e666f305375626e6574496e666f763204244163636f756e7449640100004c01186e6574756964bc0130436f6d706163743c7531363e00010c72686fbc0130436f6d706163743c7531363e0001146b61707061bc0130436f6d706163743c7531363e000128646966666963756c7479300130436f6d706163743c7536343e00013c696d6d756e6974795f706572696f64bc0130436f6d706163743c7531363e0001586d61785f616c6c6f7765645f76616c696461746f7273bc0130436f6d706163743c7531363e00014c6d696e5f616c6c6f7765645f77656967687473bc0130436f6d706163743c7531363e0001446d61785f776569676874735f6c696d6974bc0130436f6d706163743c7531363e0001447363616c696e675f6c61775f706f776572bc0130436f6d706163743c7531363e0001307375626e6574776f726b5f6ebc0130436f6d706163743c7531363e0001406d61785f616c6c6f7765645f75696473bc0130436f6d706163743c7531363e000158626c6f636b735f73696e63655f6c6173745f73746570300130436f6d706163743c7536343e00011474656d706fbc0130436f6d706163743c7531363e0001406e6574776f726b5f6d6f64616c697479bc0130436f6d706163743c7531363e00013c6e6574776f726b5f636f6e6e656374710801345665633c5b7531363b20325d3e000138656d697373696f6e5f76616c7565300130436f6d706163743c7536343e0001106275726e300130436f6d706163743c7536343e0001146f776e65720001244163636f756e7449640001206964656e74697479090601604f7074696f6e3c5375626e65744964656e7469747956323e000085080000027d0800890804184f7074696f6e040454018d080108104e6f6e6500000010536f6d6504008d0800000100008d08104070616c6c65745f73756274656e736f72207270635f696e666f2c7375626e65745f696e666f445375626e65744879706572706172616d7300006c010c72686fbc0130436f6d706163743c7531363e0001146b61707061bc0130436f6d706163743c7531363e00013c696d6d756e6974795f706572696f64bc0130436f6d706163743c7531363e00014c6d696e5f616c6c6f7765645f77656967687473bc0130436f6d706163743c7531363e0001446d61785f776569676874735f6c696d6974bc0130436f6d706163743c7531363e00011474656d706fbc0130436f6d706163743c7531363e0001386d696e5f646966666963756c7479300130436f6d706163743c7536343e0001386d61785f646966666963756c7479300130436f6d706163743c7536343e00013c776569676874735f76657273696f6e300130436f6d706163743c7536343e000148776569676874735f726174655f6c696d6974300130436f6d706163743c7536343e00014c61646a7573746d656e745f696e74657276616cbc0130436f6d706163743c7531363e00013c61637469766974795f6375746f6666bc0130436f6d706163743c7531363e000150726567697374726174696f6e5f616c6c6f776564240110626f6f6c0001607461726765745f726567735f7065725f696e74657276616cbc0130436f6d706163743c7531363e0001206d696e5f6275726e300130436f6d706163743c7536343e0001206d61785f6275726e300130436f6d706163743c7536343e000140626f6e64735f6d6f76696e675f617667300130436f6d706163743c7536343e0001486d61785f726567735f7065725f626c6f636bbc0130436f6d706163743c7531363e00014873657276696e675f726174655f6c696d6974300130436f6d706163743c7536343e0001386d61785f76616c696461746f7273bc0130436f6d706163743c7531363e00014061646a7573746d656e745f616c706861300130436f6d706163743c7536343e000128646966666963756c7479300130436f6d706163743c7536343e000150636f6d6d69745f72657665616c5f706572696f64300130436f6d706163743c7536343e000174636f6d6d69745f72657665616c5f776569676874735f656e61626c6564240110626f6f6c000128616c7068615f68696768bc0130436f6d706163743c7531363e000124616c7068615f6c6f77bc0130436f6d706163743c7531363e0001506c69717569645f616c7068615f656e61626c6564240110626f6f6c00009108000002950800950804184f7074696f6e0404540199080108104e6f6e6500000010536f6d650400990800000100009908104070616c6c65745f73756274656e736f72207270635f696e666f3064796e616d69635f696e666f2c44796e616d6963496e666f04244163636f756e7449640100005401186e6574756964bc0130436f6d706163743c7531363e0001306f776e65725f686f746b65790001244163636f756e7449640001346f776e65725f636f6c646b65790001244163636f756e74496400012c7375626e65745f6e616d659d0801405665633c436f6d706163743c75383e3e000130746f6b656e5f73796d626f6c9d0801405665633c436f6d706163743c75383e3e00011474656d706fbc0130436f6d706163743c7531363e0001246c6173745f73746570300130436f6d706163743c7536343e000158626c6f636b735f73696e63655f6c6173745f73746570300130436f6d706163743c7536343e000120656d697373696f6e300130436f6d706163743c7536343e000120616c7068615f696e300130436f6d706163743c7536343e000124616c7068615f6f7574300130436f6d706163743c7536343e00011874616f5f696e300130436f6d706163743c7536343e000148616c7068615f6f75745f656d697373696f6e300130436f6d706163743c7536343e000144616c7068615f696e5f656d697373696f6e300130436f6d706163743c7536343e00013c74616f5f696e5f656d697373696f6e300130436f6d706163743c7536343e00015870656e64696e675f616c7068615f656d697373696f6e300130436f6d706163743c7536343e00015470656e64696e675f726f6f745f656d697373696f6e300130436f6d706163743c7536343e0001347375626e65745f766f6c756d65a5080134436f6d706163743c753132383e0001546e6574776f726b5f726567697374657265645f6174300130436f6d706163743c7536343e00013c7375626e65745f6964656e74697479090601604f7074696f6e3c5375626e65744964656e7469747956323e0001306d6f76696e675f70726963658102011849393646333200009d08000002a10800a1080000060800a5080000062000a908000002ad0800ad0804184f7074696f6e04045401b1080108104e6f6e6500000010536f6d650400b1080000010000b108104070616c6c65745f73756274656e736f72207270635f696e666f246d6574616772617068244d657461677261706804244163636f756e744964010000210101186e6574756964bc0130436f6d706163743c7531363e0001106e616d659d0801405665633c436f6d706163743c75383e3e00011873796d626f6c9d0801405665633c436f6d706163743c75383e3e0001206964656e74697479090601604f7074696f6e3c5375626e65744964656e7469747956323e0001546e6574776f726b5f726567697374657265645f6174300130436f6d706163743c7536343e0001306f776e65725f686f746b65790001244163636f756e7449640001346f776e65725f636f6c646b65790001244163636f756e744964000114626c6f636b300130436f6d706163743c7536343e00011474656d706fbc0130436f6d706163743c7531363e0001246c6173745f73746570300130436f6d706163743c7536343e000158626c6f636b735f73696e63655f6c6173745f73746570300130436f6d706163743c7536343e00013c7375626e65745f656d697373696f6e300130436f6d706163743c7536343e000120616c7068615f696e300130436f6d706163743c7536343e000124616c7068615f6f7574300130436f6d706163743c7536343e00011874616f5f696e300130436f6d706163743c7536343e000148616c7068615f6f75745f656d697373696f6e300130436f6d706163743c7536343e000144616c7068615f696e5f656d697373696f6e300130436f6d706163743c7536343e00013c74616f5f696e5f656d697373696f6e300130436f6d706163743c7536343e00015870656e64696e675f616c7068615f656d697373696f6e300130436f6d706163743c7536343e00015470656e64696e675f726f6f745f656d697373696f6e300130436f6d706163743c7536343e0001347375626e65745f766f6c756d65a5080134436f6d706163743c753132383e0001306d6f76696e675f70726963658102011849393646333200010c72686fbc0130436f6d706163743c7531363e0001146b61707061bc0130436f6d706163743c7531363e00014c6d696e5f616c6c6f7765645f77656967687473bc0130436f6d706163743c7531363e0001446d61785f776569676874735f6c696d6974bc0130436f6d706163743c7531363e00013c776569676874735f76657273696f6e300130436f6d706163743c7536343e000148776569676874735f726174655f6c696d6974300130436f6d706163743c7536343e00013c61637469766974795f6375746f6666bc0130436f6d706163743c7531363e0001386d61785f76616c696461746f7273bc0130436f6d706163743c7531363e0001206e756d5f75696473bc0130436f6d706163743c7531363e0001206d61785f75696473bc0130436f6d706163743c7531363e0001106275726e300130436f6d706163743c7536343e000128646966666963756c7479300130436f6d706163743c7536343e000150726567697374726174696f6e5f616c6c6f776564240110626f6f6c000160706f775f726567697374726174696f6e5f616c6c6f776564240110626f6f6c00013c696d6d756e6974795f706572696f64bc0130436f6d706163743c7531363e0001386d696e5f646966666963756c7479300130436f6d706163743c7536343e0001386d61785f646966666963756c7479300130436f6d706163743c7536343e0001206d696e5f6275726e300130436f6d706163743c7536343e0001206d61785f6275726e300130436f6d706163743c7536343e00014061646a7573746d656e745f616c706861300130436f6d706163743c7536343e00014c61646a7573746d656e745f696e74657276616cbc0130436f6d706163743c7531363e0001607461726765745f726567735f7065725f696e74657276616cbc0130436f6d706163743c7531363e0001486d61785f726567735f7065725f626c6f636bbc0130436f6d706163743c7531363e00014873657276696e675f726174655f6c696d6974300130436f6d706163743c7536343e000174636f6d6d69745f72657665616c5f776569676874735f656e61626c6564240110626f6f6c000150636f6d6d69745f72657665616c5f706572696f64300130436f6d706163743c7536343e0001506c69717569645f616c7068615f656e61626c6564240110626f6f6c000128616c7068615f68696768bc0130436f6d706163743c7531363e000124616c7068615f6c6f77bc0130436f6d706163743c7531363e000140626f6e64735f6d6f76696e675f617667300130436f6d706163743c7536343e00011c686f746b6579735d0201385665633c4163636f756e7449643e000120636f6c646b6579735d0201385665633c4163636f756e7449643e0001286964656e746974696573b50801785665633c4f7074696f6e3c436861696e4964656e746974794f6656323e3e00011461786f6e73bd0801345665633c41786f6e496e666f3e000118616374697665c90201245665633c626f6f6c3e00014076616c696461746f725f7065726d6974c90201245665633c626f6f6c3e0001347072756e696e675f73636f7265b801445665633c436f6d706163743c7531363e3e00012c6c6173745f7570646174651d0301445665633c436f6d706163743c7536343e3e000120656d697373696f6e1d0301445665633c436f6d706163743c7536343e3e0001246469766964656e6473b801445665633c436f6d706163743c7531363e3e000128696e63656e7469766573b801445665633c436f6d706163743c7531363e3e000124636f6e73656e737573b801445665633c436f6d706163743c7531363e3e0001147472757374b801445665633c436f6d706163743c7531363e3e00011072616e6bb801445665633c436f6d706163743c7531363e3e000154626c6f636b5f61745f726567697374726174696f6e1d0301445665633c436f6d706163743c7536343e3e00012c616c7068615f7374616b651d0301445665633c436f6d706163743c7536343e3e00012474616f5f7374616b651d0301445665633c436f6d706163743c7536343e3e00012c746f74616c5f7374616b651d0301445665633c436f6d706163743c7536343e3e00016074616f5f6469766964656e64735f7065725f686f746b65793d0801785665633c284163636f756e7449642c20436f6d706163743c7536343e293e000168616c7068615f6469766964656e64735f7065725f686f746b65793d0801785665633c284163636f756e7449642c20436f6d706163743c7536343e293e0000b508000002b90800b90804184f7074696f6e04045401e5020108104e6f6e6500000010536f6d650400e5020000010000bd08000002d10200c10804184f7074696f6e04045401c5080108104e6f6e6500000010536f6d650400c5080000010000c508104070616c6c65745f73756274656e736f72207270635f696e666f2c73686f775f7375626e65742c5375626e6574537461746504244163636f756e7449640100004801186e6574756964bc0130436f6d706163743c7531363e00011c686f746b6579735d0201385665633c4163636f756e7449643e000120636f6c646b6579735d0201385665633c4163636f756e7449643e000118616374697665c90201245665633c626f6f6c3e00014076616c696461746f725f7065726d6974c90201245665633c626f6f6c3e0001347072756e696e675f73636f7265b801445665633c436f6d706163743c7531363e3e00012c6c6173745f7570646174651d0301445665633c436f6d706163743c7536343e3e000120656d697373696f6e1d0301445665633c436f6d706163743c7536343e3e0001246469766964656e6473b801445665633c436f6d706163743c7531363e3e000128696e63656e7469766573b801445665633c436f6d706163743c7531363e3e000124636f6e73656e737573b801445665633c436f6d706163743c7531363e3e0001147472757374b801445665633c436f6d706163743c7531363e3e00011072616e6bb801445665633c436f6d706163743c7531363e3e000154626c6f636b5f61745f726567697374726174696f6e1d0301445665633c436f6d706163743c7536343e3e00012c616c7068615f7374616b651d0301445665633c436f6d706163743c7536343e3e00012474616f5f7374616b651d0301445665633c436f6d706163743c7536343e3e00012c746f74616c5f7374616b651d0301445665633c436f6d706163743c7536343e3e000140656d697373696f6e5f686973746f7279c90801585665633c5665633c436f6d706163743c7536343e3e3e0000c9080000021d0300cd08000002d10800d108104070616c6c65745f73756274656e736f72207270635f696e666f287374616b655f696e666f245374616b65496e666f04244163636f756e744964010000200118686f746b65790001244163636f756e74496400011c636f6c646b65790001244163636f756e7449640001186e6574756964bc0130436f6d706163743c7531363e0001147374616b65300130436f6d706163743c7536343e0001186c6f636b6564300130436f6d706163743c7536343e000120656d697373696f6e300130436f6d706163743c7536343e000114647261696e300130436f6d706163743c7536343e00013469735f72656769737465726564240110626f6f6c0000d508000002d90800d9080000040800cd0800dd0804184f7074696f6e04045401d1080108104e6f6e6500000010536f6d650400d1080000010000e10808586e6f64655f73756274656e736f725f72756e74696d653052756e74696d654572726f720001501853797374656d0400a90101706672616d655f73797374656d3a3a4572726f723c52756e74696d653e0000001c4772616e64706104000502017870616c6c65745f6772616e6470613a3a4572726f723c52756e74696d653e0004002042616c616e63657304006502017c70616c6c65745f62616c616e6365733a3a4572726f723c52756e74696d653e0005003c53756274656e736f724d6f64756c6504000d06018070616c6c65745f73756274656e736f723a3a4572726f723c52756e74696d653e0007002c547269756d7669726174650400190601fc70616c6c65745f636f6c6c6563746976653a3a4572726f723c52756e74696d652c2070616c6c65745f636f6c6c6563746976653a3a496e7374616e6365313e00080048547269756d7669726174654d656d626572730400210601fc70616c6c65745f6d656d626572736869703a3a4572726f723c52756e74696d652c2070616c6c65745f6d656d626572736869703a3a496e7374616e6365313e0009003453656e6174654d656d626572730400290601fc70616c6c65745f6d656d626572736869703a3a4572726f723c52756e74696d652c2070616c6c65745f6d656d626572736869703a3a496e7374616e6365323e000a001c5574696c69747904002d06017870616c6c65745f7574696c6974793a3a4572726f723c52756e74696d653e000b00105375646f04003106016c70616c6c65745f7375646f3a3a4572726f723c52756e74696d653e000c00204d756c746973696704004106017c70616c6c65745f6d756c74697369673a3a4572726f723c52756e74696d653e000d0020507265696d61676504006906017c70616c6c65745f707265696d6167653a3a4572726f723c52756e74696d653e000e00245363686564756c657204008d06018070616c6c65745f7363686564756c65723a3a4572726f723c52756e74696d653e000f001450726f78790400b106017070616c6c65745f70726f78793a3a4572726f723c52756e74696d653e0010002052656769737472790400b906017c70616c6c65745f72656769737472793a3a4572726f723c52756e74696d653e0011002c436f6d6d69746d656e74730400c106018870616c6c65745f636f6d6d69746d656e74733a3a4572726f723c52756e74696d653e0012002841646d696e5574696c730400c506018870616c6c65745f61646d696e5f7574696c733a3a4572726f723c52756e74696d653e00130020536166654d6f64650400d106018070616c6c65745f736166655f6d6f64653a3a4572726f723c52756e74696d653e00140020457468657265756d04001507017c70616c6c65745f657468657265756d3a3a4572726f723c52756e74696d653e0015000c45564d04002107016870616c6c65745f65766d3a3a4572726f723c52756e74696d653e001600144472616e6404002507017070616c6c65745f6472616e643a3a4572726f723c52756e74696d653e001a0000681853797374656d011853797374656d481c4163636f756e7401010402000ce0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008004e8205468652066756c6c206163636f756e7420696e666f726d6174696f6e20666f72206120706172746963756c6172206163636f756e742049442e3845787472696e736963436f756e74000010040004b820546f74616c2065787472696e7369637320636f756e7420666f72207468652063757272656e7420626c6f636b2e40496e686572656e74734170706c696564010024040004a4205768657468657220616c6c20696e686572656e74732068617665206265656e206170706c6965642e2c426c6f636b576569676874010028180000000000000488205468652063757272656e742077656967687420666f722074686520626c6f636b2e40416c6c45787472696e736963734c656e000010040004410120546f74616c206c656e6774682028696e2062797465732920666f7220616c6c2065787472696e736963732070757420746f6765746865722c20666f72207468652063757272656e7420626c6f636b2e24426c6f636b486173680101040510348000000000000000000000000000000000000000000000000000000000000000000498204d6170206f6620626c6f636b206e756d6265727320746f20626c6f636b206861736865732e3445787472696e736963446174610101040510380400043d012045787472696e73696373206461746120666f72207468652063757272656e7420626c6f636b20286d61707320616e2065787472696e736963277320696e64657820746f206974732064617461292e184e756d6265720100101000000000040901205468652063757272656e7420626c6f636b206e756d626572206265696e672070726f6365737365642e205365742062792060657865637574655f626c6f636b602e28506172656e744861736801003480000000000000000000000000000000000000000000000000000000000000000004702048617368206f66207468652070726576696f757320626c6f636b2e1844696765737401003c040004f020446967657374206f66207468652063757272656e7420626c6f636b2c20616c736f2070617274206f662074686520626c6f636b206865616465722e184576656e747301004c04001ca0204576656e7473206465706f736974656420666f72207468652063757272656e7420626c6f636b2e001d01204e4f54453a20546865206974656d20697320756e626f756e6420616e642073686f756c64207468657265666f7265206e657665722062652072656164206f6e20636861696e2ed020497420636f756c64206f746865727769736520696e666c6174652074686520506f562073697a65206f66206120626c6f636b2e002d01204576656e747320686176652061206c6172676520696e2d6d656d6f72792073697a652e20426f7820746865206576656e747320746f206e6f7420676f206f75742d6f662d6d656d6f7279fc206a75737420696e206361736520736f6d656f6e65207374696c6c207265616473207468656d2066726f6d2077697468696e207468652072756e74696d652e284576656e74436f756e74010010100000000004b820546865206e756d626572206f66206576656e747320696e2074686520604576656e74733c543e60206c6973742e2c4576656e74546f70696373010104023459010400282501204d617070696e67206265747765656e206120746f7069632028726570726573656e74656420627920543a3a486173682920616e64206120766563746f72206f6620696e646578657394206f66206576656e747320696e2074686520603c4576656e74733c543e3e60206c6973742e00510120416c6c20746f70696320766563746f727320686176652064657465726d696e69737469632073746f72616765206c6f636174696f6e7320646570656e64696e67206f6e2074686520746f7069632e2054686973450120616c6c6f7773206c696768742d636c69656e747320746f206c6576657261676520746865206368616e67657320747269652073746f7261676520747261636b696e67206d656368616e69736d20616e64e420696e2063617365206f66206368616e67657320666574636820746865206c697374206f66206576656e7473206f6620696e7465726573742e005901205468652076616c756520686173207468652074797065206028426c6f636b4e756d626572466f723c543e2c204576656e74496e646578296020626563617573652069662077652075736564206f6e6c79206a7573744d012074686520604576656e74496e64657860207468656e20696e20636173652069662074686520746f70696320686173207468652073616d6520636f6e74656e7473206f6e20746865206e65787420626c6f636b0101206e6f206e6f74696669636174696f6e2077696c6c20626520747269676765726564207468757320746865206576656e74206d69676874206265206c6f73742e484c61737452756e74696d655570677261646500005d0104000455012053746f726573207468652060737065635f76657273696f6e6020616e642060737065635f6e616d6560206f66207768656e20746865206c6173742072756e74696d6520757067726164652068617070656e65642e545570677261646564546f553332526566436f756e740100240400044d012054727565206966207765206861766520757067726164656420736f207468617420607479706520526566436f756e74602069732060753332602e2046616c7365202864656661756c7429206966206e6f742e605570677261646564546f547269706c65526566436f756e740100240400085d012054727565206966207765206861766520757067726164656420736f2074686174204163636f756e74496e666f20636f6e7461696e73207468726565207479706573206f662060526566436f756e74602e2046616c736548202864656661756c7429206966206e6f742e38457865637574696f6e506861736500005501040004882054686520657865637574696f6e207068617365206f662074686520626c6f636b2e44417574686f72697a65645570677261646500006501040004b82060536f6d6560206966206120636f6465207570677261646520686173206265656e20617574686f72697a65642e01690101581830426c6f636b576569676874737901f901624d186c000b00409452a30313ffffffffffffffff4247871900010b30beb1555d021366666666666666a6010b0030ef7dba0213ffffffffffffffbf0100004247871900010b30ce562a46031366666666666666e6010b00409452a30313ffffffffffffffff01070010a5d4e8130000000000000040424787190000000004d020426c6f636b20262065787472696e7369637320776569676874733a20626173652076616c75657320616e64206c696d6974732e2c426c6f636b4c656e677468890130000078000000a0000000a00004a820546865206d6178696d756d206c656e677468206f66206120626c6f636b2028696e206279746573292e38426c6f636b48617368436f756e74101060090000045501204d6178696d756d206e756d626572206f6620626c6f636b206e756d62657220746f20626c6f636b2068617368206d617070696e677320746f206b65657020286f6c64657374207072756e6564206669727374292e20446257656967687491014040787d010000000000e1f505000000000409012054686520776569676874206f662072756e74696d65206461746162617365206f7065726174696f6e73207468652072756e74696d652063616e20696e766f6b652e1c56657273696f6e95015104386e6f64652d73756274656e736f72386e6f64652d73756274656e736f7201000000e9000000010000004cdf6acb689907609b0500000037e397fc7c91f5e40200000040fe3ad401f8959a06000000fbc577b9d747efd601000000d2bc9897eed08f1503000000f78b278be53f454c02000000dd718d5cc53262d401000000ab3c0572291feb8b01000000ed99c5acb25eedf503000000bc9d89904f5b923f0100000037c8bb1350a9a2a804000000f3ff14d5ab52705903000000582211f65bb14b8905000000e65b00e46cedd0aa0200000042e62be4a39e5b6001000000806df4ccaa9ed485010000008375104b299b74c5010000005d1fbfbe852f280701000000c6886e2f8e598b0a0100000001000000010484204765742074686520636861696e277320696e2d636f64652076657273696f6e2e28535335385072656669789c082a0014a8205468652064657369676e61746564205353353820707265666978206f66207468697320636861696e2e0039012054686973207265706c6163657320746865202273733538466f726d6174222070726f7065727479206465636c6172656420696e2074686520636861696e20737065632e20526561736f6e20697331012074686174207468652072756e74696d652073686f756c64206b6e6f772061626f7574207468652070726566697820696e206f7264657220746f206d616b6520757365206f662069742061737020616e206964656e746966696572206f662074686520636861696e2e01a90100006052616e646f6d6e657373436f6c6c656374697665466c6970016052616e646f6d6e657373436f6c6c656374697665466c6970043852616e646f6d4d6174657269616c0100ad0104000c610120536572696573206f6620626c6f636b20686561646572732066726f6d20746865206c61737420383120626c6f636b73207468617420616374732061732072616e646f6d2073656564206d6174657269616c2e2054686973610120697320617272616e67656420617320612072696e672062756666657220776974682060626c6f636b5f6e756d626572202520383160206265696e672074686520696e64657820696e746f20746865206056656360206f664420746865206f6c6465737420686173682e0000000001002454696d657374616d70012454696d657374616d70080c4e6f7701001820000000000000000004a0205468652063757272656e742074696d6520666f72207468652063757272656e7420626c6f636b2e24446964557064617465010024040010d82057686574686572207468652074696d657374616d7020686173206265656e207570646174656420696e207468697320626c6f636b2e00550120546869732076616c7565206973207570646174656420746f206074727565602075706f6e207375636365737366756c207375626d697373696f6e206f6620612074696d657374616d702062792061206e6f64652e4501204974206973207468656e20636865636b65642061742074686520656e64206f66206561636820626c6f636b20657865637574696f6e20696e2074686520606f6e5f66696e616c697a656020686f6f6b2e01b1010004344d696e696d756d506572696f6418207017000000000000188c20546865206d696e696d756d20706572696f64206265747765656e20626c6f636b732e004d012042652061776172652074686174207468697320697320646966666572656e7420746f20746865202a65787065637465642a20706572696f6420746861742074686520626c6f636b2070726f64756374696f6e4901206170706172617475732070726f76696465732e20596f75722063686f73656e20636f6e73656e7375732073797374656d2077696c6c2067656e6572616c6c7920776f726b2077697468207468697320746f61012064657465726d696e6520612073656e7369626c6520626c6f636b2074696d652e20466f72206578616d706c652c20696e2074686520417572612070616c6c65742069742077696c6c20626520646f75626c6520746869737020706572696f64206f6e2064656661756c742073657474696e67732e0002001041757261011041757261082c417574686f7269746965730100b5010400046c205468652063757272656e7420617574686f72697479207365742e2c43757272656e74536c6f740100c1012000000000000000000c80205468652063757272656e7420736c6f74206f66207468697320626c6f636b2e009420546869732077696c6c2062652073657420696e20606f6e5f696e697469616c697a65602e00000430536c6f744475726174696f6e1820e02e000000000000100d012054686520736c6f74206475726174696f6e20417572612073686f756c642072756e20776974682c2065787072657373656420696e206d696c6c697365636f6e64732e3d0120546865206566666563746976652076616c7565206f66207468697320747970652073686f756c64206e6f74206368616e6765207768696c652074686520636861696e2069732072756e6e696e672e00350120466f72206261636b776172647320636f6d7061746962696c6974792065697468657220757365205b604d696e696d756d506572696f6454696d657354776f605d206f72206120636f6e73742e0003001c4772616e647061011c4772616e6470611c1453746174650100c50104000490205374617465206f66207468652063757272656e7420617574686f72697479207365742e3450656e64696e674368616e67650000c901040004c42050656e64696e67206368616e67653a20287369676e616c65642061742c207363686564756c6564206368616e6765292e284e657874466f72636564000010040004bc206e65787420626c6f636b206e756d6265722077686572652077652063616e20666f7263652061206368616e67652e1c5374616c6c65640000e40400049020607472756560206966207765206172652063757272656e746c79207374616c6c65642e3043757272656e745365744964010018200000000000000000085d0120546865206e756d626572206f66206368616e6765732028626f746820696e207465726d73206f66206b65797320616e6420756e6465726c79696e672065636f6e6f6d696320726573706f6e736962696c697469657329c420696e20746865202273657422206f66204772616e6470612076616c696461746f72732066726f6d2067656e657369732e30536574496453657373696f6e00010405181004002859012041206d617070696e672066726f6d206772616e6470612073657420494420746f2074686520696e646578206f6620746865202a6d6f737420726563656e742a2073657373696f6e20666f722077686963682069747368206d656d62657273207765726520726573706f6e7369626c652e0045012054686973206973206f6e6c79207573656420666f722076616c69646174696e672065717569766f636174696f6e2070726f6f66732e20416e2065717569766f636174696f6e2070726f6f66206d7573744d0120636f6e7461696e732061206b65792d6f776e6572736869702070726f6f6620666f72206120676976656e2073657373696f6e2c207468657265666f7265207765206e65656420612077617920746f20746965450120746f6765746865722073657373696f6e7320616e64204752414e44504120736574206964732c20692e652e207765206e65656420746f2076616c6964617465207468617420612076616c696461746f7241012077617320746865206f776e6572206f66206120676976656e206b6579206f6e206120676976656e2073657373696f6e2c20616e642077686174207468652061637469766520736574204944207761735420647572696e6720746861742073657373696f6e2e00b82054574f582d4e4f54453a2060536574496460206973206e6f7420756e646572207573657220636f6e74726f6c2e2c417574686f7269746965730100cd0104000484205468652063757272656e74206c697374206f6620617574686f7269746965732e01d501017c0c384d6178417574686f726974696573101020000000045c204d617820417574686f72697469657320696e20757365344d61784e6f6d696e61746f727310101400000004d420546865206d6178696d756d206e756d626572206f66206e6f6d696e61746f727320666f7220656163682076616c696461746f722e584d6178536574496453657373696f6e456e74726965731820000000000000000018390120546865206d6178696d756d206e756d626572206f6620656e747269657320746f206b65657020696e207468652073657420696420746f2073657373696f6e20696e646578206d617070696e672e0031012053696e6365207468652060536574496453657373696f6e60206d6170206973206f6e6c79207573656420666f722076616c69646174696e672065717569766f636174696f6e73207468697329012076616c75652073686f756c642072656c61746520746f2074686520626f6e64696e67206475726174696f6e206f66207768617465766572207374616b696e672073797374656d2069733501206265696e6720757365642028696620616e79292e2049662065717569766f636174696f6e2068616e646c696e67206973206e6f7420656e61626c6564207468656e20746869732076616c7565342063616e206265207a65726f2e01050204002042616c616e636573012042616c616e6365731c34546f74616c49737375616e636501001820000000000000000004982054686520746f74616c20756e6974732069737375656420696e207468652073797374656d2e40496e61637469766549737375616e63650100182000000000000000000409012054686520746f74616c20756e697473206f66206f75747374616e64696e672064656163746976617465642062616c616e636520696e207468652073797374656d2e1c4163636f756e74010104020014a000000000000000000000000000000000000000000000000000000000000000000000000000000080600901205468652042616c616e6365732070616c6c6574206578616d706c65206f662073746f72696e67207468652062616c616e6365206f6620616e206163636f756e742e00282023204578616d706c650034206060606e6f636f6d70696c65b02020696d706c2070616c6c65745f62616c616e6365733a3a436f6e66696720666f722052756e74696d65207b19022020202074797065204163636f756e7453746f7265203d2053746f726167654d61705368696d3c53656c663a3a4163636f756e743c52756e74696d653e2c206672616d655f73797374656d3a3a50726f76696465723c52756e74696d653e2c204163636f756e7449642c2053656c663a3a4163636f756e74446174613c42616c616e63653e3e0c20207d102060606000150120596f752063616e20616c736f2073746f7265207468652062616c616e6365206f6620616e206163636f756e7420696e20746865206053797374656d602070616c6c65742e00282023204578616d706c650034206060606e6f636f6d70696c65b02020696d706c2070616c6c65745f62616c616e6365733a3a436f6e66696720666f722052756e74696d65207b7420202074797065204163636f756e7453746f7265203d2053797374656d0c20207d102060606000510120427574207468697320636f6d657320776974682074726164656f6666732c2073746f72696e67206163636f756e742062616c616e63657320696e207468652073797374656d2070616c6c65742073746f7265736d0120606672616d655f73797374656d60206461746120616c6f6e677369646520746865206163636f756e74206461746120636f6e747261727920746f2073746f72696e67206163636f756e742062616c616e63657320696e207468652901206042616c616e636573602070616c6c65742c20776869636820757365732061206053746f726167654d61706020746f2073746f72652062616c616e6365732064617461206f6e6c792e4101204e4f54453a2054686973206973206f6e6c79207573656420696e207468652063617365207468617420746869732070616c6c6574206973207573656420746f2073746f72652062616c616e6365732e144c6f636b7301010402000902040010b820416e79206c6971756964697479206c6f636b73206f6e20736f6d65206163636f756e742062616c616e6365732e2501204e4f54453a2053686f756c64206f6e6c79206265206163636573736564207768656e2073657474696e672c206368616e67696e6720616e642066726565696e672061206c6f636b2e00ad0120557365206f66206c6f636b73206973206465707265636174656420696e206661766f7572206f6620667265657a65732e20536565206068747470733a2f2f6769746875622e636f6d2f706172697479746563682f7375627374726174652f70756c6c2f31323935312f602052657365727665730101040200190204000ca4204e616d6564207265736572766573206f6e20736f6d65206163636f756e742062616c616e6365732e00b10120557365206f66207265736572766573206973206465707265636174656420696e206661766f7572206f6620686f6c64732e20536565206068747470733a2f2f6769746875622e636f6d2f706172697479746563682f7375627374726174652f70756c6c2f31323935312f6014486f6c6473010104020025020400046c20486f6c6473206f6e206163636f756e742062616c616e6365732e1c467265657a6573010104020041020400048820467265657a65206c6f636b73206f6e206163636f756e742062616c616e6365732e015102018c10484578697374656e7469616c4465706f7369741820f40100000000000020410120546865206d696e696d756d20616d6f756e7420726571756972656420746f206b65657020616e206163636f756e74206f70656e2e204d5553542042452047524541544552205448414e205a45524f2100590120496620796f75202a7265616c6c792a206e65656420697420746f206265207a65726f2c20796f752063616e20656e61626c652074686520666561747572652060696e7365637572655f7a65726f5f65646020666f72610120746869732070616c6c65742e20486f77657665722c20796f7520646f20736f20617420796f7572206f776e207269736b3a20746869732077696c6c206f70656e2075702061206d616a6f7220446f5320766563746f722e590120496e206361736520796f752068617665206d756c7469706c6520736f7572636573206f662070726f7669646572207265666572656e6365732c20796f75206d617920616c736f2067657420756e65787065637465648c206265686176696f757220696620796f7520736574207468697320746f207a65726f2e00f020426f74746f6d206c696e653a20446f20796f757273656c662061206661766f757220616e64206d616b65206974206174206c65617374206f6e6521204d61784c6f636b7310103200000010f420546865206d6178696d756d206e756d626572206f66206c6f636b7320746861742073686f756c64206578697374206f6e20616e206163636f756e742edc204e6f74207374726963746c7920656e666f726365642c20627574207573656420666f722077656967687420657374696d6174696f6e2e00ad0120557365206f66206c6f636b73206973206465707265636174656420696e206661766f7572206f6620667265657a65732e20536565206068747470733a2f2f6769746875622e636f6d2f706172697479746563682f7375627374726174652f70756c6c2f31323935312f602c4d617852657365727665731010320000000c0d0120546865206d6178696d756d206e756d626572206f66206e616d656420726573657276657320746861742063616e206578697374206f6e20616e206163636f756e742e00b10120557365206f66207265736572766573206973206465707265636174656420696e206661766f7572206f6620686f6c64732e20536565206068747470733a2f2f6769746875622e636f6d2f706172697479746563682f7375627374726174652f70756c6c2f31323935312f60284d6178467265657a657310103200000004610120546865206d6178696d756d206e756d626572206f6620696e646976696475616c20667265657a65206c6f636b7320746861742063616e206578697374206f6e20616e206163636f756e7420617420616e792074696d652e0165020500485472616e73616374696f6e5061796d656e7401485472616e73616374696f6e5061796d656e7408444e6578744665654d756c7469706c6965720100690240000064a7b3b6e00d0000000000000000003853746f7261676556657273696f6e01006d0204000000019404604f7065726174696f6e616c4665654d756c7469706c696572080405545901204120666565206d756c7469706c69657220666f7220604f7065726174696f6e616c602065787472696e7369637320746f20636f6d7075746520227669727475616c207469702220746f20626f6f73742074686569722c20607072696f726974796000510120546869732076616c7565206973206d756c7469706c69656420627920746865206066696e616c5f6665656020746f206f627461696e206120227669727475616c20746970222074686174206973206c61746572f420616464656420746f20612074697020636f6d706f6e656e7420696e20726567756c617220607072696f72697479602063616c63756c6174696f6e732e4d01204974206d65616e732074686174206120604e6f726d616c60207472616e73616374696f6e2063616e2066726f6e742d72756e20612073696d696c61726c792d73697a656420604f7065726174696f6e616c6041012065787472696e736963202877697468206e6f20746970292c20627920696e636c7564696e672061207469702076616c75652067726561746572207468616e20746865207669727475616c207469702e003c20606060727573742c69676e6f726540202f2f20466f7220604e6f726d616c608c206c6574207072696f72697479203d207072696f726974795f63616c6328746970293b0054202f2f20466f7220604f7065726174696f6e616c601101206c6574207669727475616c5f746970203d2028696e636c7573696f6e5f666565202b2074697029202a204f7065726174696f6e616c4665654d756c7469706c6965723bc4206c6574207072696f72697479203d207072696f726974795f63616c6328746970202b207669727475616c5f746970293b1020606060005101204e6f746520746861742073696e636520776520757365206066696e616c5f6665656020746865206d756c7469706c696572206170706c69657320616c736f20746f2074686520726567756c61722060746970605d012073656e74207769746820746865207472616e73616374696f6e2e20536f2c206e6f74206f6e6c7920646f657320746865207472616e73616374696f6e206765742061207072696f726974792062756d702062617365646101206f6e207468652060696e636c7573696f6e5f666565602c2062757420776520616c736f20616d706c6966792074686520696d70616374206f662074697073206170706c69656420746f20604f7065726174696f6e616c6038207472616e73616374696f6e732e0006003c53756274656e736f724d6f64756c65013c53756274656e736f724d6f64756c6559026c436f6c646b6579537761705363686564756c654475726174696f6e01001010a08c0000007c446973736f6c76654e6574776f726b5363686564756c654475726174696f6e01001010a08c0000007453656e61746552657175697265645374616b6550657263656e74616765010018200100000000000000002454616f57656967687401001820fc62e03efa3c7c0d3474203d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d70203d3d3d3d205374616b696e67205661726961626c6573203d3d3d3d74203d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d8901205468652053756274656e736f72205b60546f74616c49737375616e6365605d20726570726573656e74732074686520746f74616c2069737375616e6365206f6620746f6b656e73206f6e207468652042697474656e736f72206e6574776f726b2e008020497420697320636f6d707269736564206f662074687265652070617274733a6501202d2054686520746f74616c20616d6f756e74206f662069737375656420746f6b656e732c20747261636b656420696e2074686520546f74616c49737375616e6365206f66207468652042616c616e6365732070616c6c65743501202d2054686520746f74616c20616d6f756e74206f6620746f6b656e73207374616b656420696e207468652073797374656d2c20747261636b656420696e205b60546f74616c5374616b65605d0102202d2054686520746f74616c20616d6f756e74206f6620746f6b656e73206c6f636b656420757020666f72207375626e6574207265672c20747261636b656420696e205b60546f74616c5375626e65744c6f636b6564605d2061747461696e656420627920697465726174696e67206f766572207375626e6574206c6f636b2e007501204576656e7475616c6c792c2042697474656e736f722073686f756c64206d69677261746520746f207573696e6720486f6c647320616674657277686963682074696d652077652077696c6c206e6f742072657175697265207468697354207365706172617465206163636f756e74696e672e6c202d2d2d204954454d202d2d3e20476c6f62616c207765696768743c4d617844656c656761746554616b6501009c08142e048c202d2d2d204954454d20282064656661756c745f64656c65676174655f74616b6520293c4d696e44656c656761746554616b6501009c080000047c202d2d2d204954454d2028206d696e5f64656c65676174655f74616b6520293c4d61784368696c646b657954616b6501009c08142e048c202d2d2d204954454d20282064656661756c745f6368696c646b65795f74616b6520293c4d696e4368696c646b657954616b6501009c080000047c202d2d2d204954454d2028206d696e5f6368696c646b65795f74616b652029144f776e6572010104020000800000000000000000000000000000000000000000000000000000000000000000041501204d4150202820686f742029202d2d3e20636f6c64207c2052657475726e732074686520636f6e74726f6c6c696e6720636f6c646b657920666f72206120686f746b65792e2444656c65676174657301010402009c08142e04b501204d4150202820686f742029202d2d3e2074616b65207c2052657475726e732074686520686f746b65792064656c65676174696f6e2074616b652e20416e64207369676e616c7320746861742074686973206b6579206973206f70656e20666f722064656c65676174696f6e2e304368696c646b657954616b65010108020671029c080000045d0120444d4150202820686f742c206e65747569642029202d2d3e2074616b65207c2052657475726e732074686520686f746b6579206368696c646b65792074616b6520666f722061207370656369666963207375626e65744050656e64696e674368696c644b65797301010806027502790224000000000000000000041d0120444d41502028206e65747569642c20706172656e742029202d2d3e20285665633c2870726f706f7274696f6e2c6368696c64293e2c20636f6f6c5f646f776e5f626c6f636b29244368696c644b65797301010802067102ac040004d020444d4150202820706172656e742c206e65747569642029202d2d3e205665633c2870726f706f7274696f6e2c6368696c64293e28506172656e744b65797301010802067102ac040004d020444d41502028206368696c642c206e65747569642029202d2d3e205665633c2870726f706f7274696f6e2c706172656e74293e5c416c7068614469766964656e64735065725375626e65740101080602750218200000000000000000005454616f4469766964656e64735065725375626e657401010806027502182000000000000000000034426c6f636b456d697373696f6e0100182000ca9a3b00000000104c203d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d4c203d3d3d3d20436f696e62617365203d3d3d3d4c203d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d8c202d2d2d204954454d202820676c6f62616c5f626c6f636b5f656d697373696f6e2029684c617374486f746b6579456d697373696f6e4f6e4e65747569640101080206710218200000000000000000042501202d2d2d20444d6170202820686f742c206e65747569642029202d2d3e20656d697373696f6e207c206c61737420686f746b657920656d697373696f6e206f6e206e6574776f726b2e844c617374486f746b6579436f6c646b6579456d697373696f6e4f6e4e657475696401010c0202067d021820000000000000000004d101202d2d2d204e4d4150202820686f742c20636f6c642c206e65747569642029202d2d3e206c6173745f656d697373696f6e5f6f6e5f686f745f636f6c645f6e6574207c2052657475726e7320746865206c6173745f656d697373696f6e5f7570646174655f6f6e5f686f745f636f6c645f6e657434546f74616c49737375616e6365010018200000000000000000306c203d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d6c203d3d3d3d205374616b696e6720436f756e74657273203d3d3d3d6c203d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d8901205468652053756274656e736f72205b60546f74616c49737375616e6365605d20726570726573656e74732074686520746f74616c2069737375616e6365206f6620746f6b656e73206f6e207468652042697474656e736f72206e6574776f726b2e008020497420697320636f6d707269736564206f662074687265652070617274733a6501202d2054686520746f74616c20616d6f756e74206f662069737375656420746f6b656e732c20747261636b656420696e2074686520546f74616c49737375616e6365206f66207468652042616c616e6365732070616c6c65743501202d2054686520746f74616c20616d6f756e74206f6620746f6b656e73207374616b656420696e207468652073797374656d2c20747261636b656420696e205b60546f74616c5374616b65605d0102202d2054686520746f74616c20616d6f756e74206f6620746f6b656e73206c6f636b656420757020666f72207375626e6574207265672c20747261636b656420696e205b60546f74616c5375626e65744c6f636b6564605d2061747461696e656420627920697465726174696e67206f766572207375626e6574206c6f636b2e007501204576656e7475616c6c792c2042697474656e736f722073686f756c64206d69677261746520746f207573696e6720486f6c647320616674657277686963682074696d652077652077696c6c206e6f742072657175697265207468697354207365706172617465206163636f756e74696e672e28546f74616c5374616b65010018200000000000000000003044796e616d6963426c6f636b01001820000000000000000000445375626e65744d6f76696e67416c70686101008102405532000000000000000000000000000000445375626e65744d6f76696e675072696365010104069c8102400000000000000000000000000000000000305375626e6574566f6c756d65010104069c20400000000000000000000000000000000000245375626e657454414f010104069c1820000000000000000000545375626e6574416c706861496e456d697373696f6e010104069c1820000000000000000000585375626e6574416c7068614f7574456d697373696f6e010104069c18200000000000000000004c5375626e657454616f496e456d697373696f6e010104069c18200000000000000000005c5375626e6574416c706861456d697373696f6e53656c6c010104069c18200000000000000000004c546f74616c5374616b65417444796e616d6963010104069c1820000000000000000000345375626e6574416c706861496e010104069c1820000000000000000000385375626e6574416c7068614f7574010104069c1820000000000000000000385374616b696e67486f746b65797301010402005d02040000304f776e6564486f746b65797301010402005d02040000145374616b650101080206ad02182000000000000000000489012028444550524543415445442920444d4150202820686f742c20636f6c642029202d2d3e207374616b65207c2052657475726e7320746865207374616b6520756e646572206120636f6c646b657920707265666978656420627920686f746b65792e50436f6c646b6579537761705363686564756c65640101040200a4000040546f74616c486f746b6579416c70686101010802067102182000000000000000000044546f74616c486f746b657953686172657301010802067102b102400000000000000000000000000000000004ad0120444d4150202820686f742c206e65747569642029202d2d3e20746f74616c5f616c7068615f736861726573207c2052657475726e7320746865206e756d626572206f6620616c7068612073686172657320666f72206120686f746b6579206f6e2061207375626e65742e14416c70686101010c0202067d02b1024000000000000000000000000000000000002c546f6b656e53796d626f6c010104069c381410f09d9c8f00285375626e65744e616d65010104069c381410f09d9c8f002055736564576f726b0101040638182000000000000000001074203d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d74203d3d3d3d20476c6f62616c20506172616d6574657273203d3d3d3d3d74203d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d88202d2d2d2053746f726167654974656d20476c6f62616c205573656420576f726b2e604d6178526567697374726174696f6e73506572426c6f636b010104069c9c08010004bc202d2d2d204954454d2820676c6f62616c5f6d61785f726567697374726174696f6e735f7065725f626c6f636b20292c5375626e65744c696d697401009c080c00049c202d2d2d204954454d28206d6178696d756d5f6e756d6265725f6f665f6e6574776f726b73202934546f74616c4e6574776f726b7301009c08000004b8202d2d2d204954454d2820746f74616c5f6e756d6265725f6f665f6578697374696e675f6e6574776f726b732029544e6574776f726b496d6d756e697479506572696f6401001820e0c40000000000000480204954454d28206e6574776f726b5f696d6d756e6974795f706572696f642029544e6574776f726b4c617374526567697374657265640100182000000000000000000498204954454d28206e6574776f726b5f6c6173745f726567697374657265645f626c6f636b2029544e6574776f726b4d696e416c6c6f7765645569647301009c0880000484204954454d28206e6574776f726b5f6d696e5f616c6c6f7765645f756964732029484e6574776f726b4d696e4c6f636b436f7374010018200010a5d4e80000000478204954454d28206d696e5f6e6574776f726b5f6c6f636b5f636f737420294c4e6574776f726b4c6173744c6f636b436f7374010018200010a5d4e8000000047c204954454d28206c6173745f6e6574776f726b5f6c6f636b5f636f73742029704e6574776f726b4c6f636b526564756374696f6e496e74657276616c01001820c08901000000000004a0204954454d28206e6574776f726b5f6c6f636b5f726564756374696f6e5f696e74657276616c2029385375626e65744f776e657243757401009c08142e0464204954454d28207375626e65745f6f776e65725f6375742029404e6574776f726b526174654c696d697401001820201c000000000000046c204954454d28206e6574776f726b5f726174655f6c696d69742029644e6f6d696e61746f724d696e52657175697265645374616b6501001820000000000000000000385472616e73666572546f67676c65010104069c2404010c74203d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d60203d3d3d3d205375626e6574204c6f636b73203d3d3d3d3d74203d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d305375626e65744c6f636b6564010104069c1820000000000000000000344c6172676573744c6f636b6564010104069c18200000000000000000002041766754656d706f01009c0863000c48203d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d48203d3d3d3d2054656d706f73203d3d3d3d3d48203d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d204d617854656d706f01009c081e00001454656d706f010104069c9c086300003c5375626e65744d656368616e69736d010104069c9c0800000c74203d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d74203d3d3d3d205375626e657420506172616d6574657273203d3d3d3d3d74203d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d2c5375626e6574776f726b4e010104069c9c080000041501202d2d2d204d41502028206e65747569642029202d2d3e207375626e6574776f726b5f6e20284e756d626572206f66205549447320696e20746865206e6574776f726b292e3c4e6574776f726b4d6f64616c697479010104069c9c08000004fc202d2d2d204d41502028206e65747569642029202d2d3e206d6f64616c697479202020544558543a20302c20494d4147453a20312c2054454e534f523a2032344e6574776f726b734164646564010104069c24040004a0202d2d2d204d41502028206e65747569642029202d2d3e206e6574776f726b5f69735f61646465643c49734e6574776f726b4d656d626572010108020671022404000494202d2d2d20444d4150202820686f746b65792c206e65747569642029202d2d3e20626f6f6c684e6574776f726b526567697374726174696f6e416c6c6f776564010104069c24040004d0202d2d2d204d41502028206e65747569642029202d2d3e206e6574776f726b5f726567697374726174696f6e5f616c6c6f776564744e6574776f726b506f77526567697374726174696f6e416c6c6f776564010104069c24040004ac202d2d2d204d41502028206e65747569642029202d2d3e206e6574776f726b5f706f775f616c6c6f7765644c4e6574776f726b526567697374657265644174010104069c182000000000000000000494202d2d2d204d41502028206e65747569642029202d2d3e20626c6f636b5f6372656174656438456d697373696f6e56616c756573010104069c18200000000000000000049c202d2d2d204d41502028206e65747569642029202d2d3e20656d697373696f6e5f76616c7565733c50656e64696e67456d697373696f6e010104069c1820000000000000000004a0202d2d2d204d41502028206e65747569642029202d2d3e2070656e64696e675f656d697373696f6e3c50656e64696e67526f6f7444697673010104069c1820000000000000000004b4202d2d2d204d41502028206e65747569642029202d2d3e2070656e64696e675f726f6f745f656d697373696f6e4c50656e64696e67416c70686153776170706564010104069c1820000000000000000004b4202d2d2d204d41502028206e65747569642029202d2d3e2070656e64696e675f616c7068615f737761707065643c50656e64696e674f776e6572437574010104069c1820000000000000000004a4202d2d2d204d41502028206e65747569642029202d2d3e2070656e64696e675f6f776e65725f6375744c426c6f636b7353696e63654c61737453746570010104069c1820000000000000000004b8202d2d2d204d41502028206e65747569642029202d2d3e20626c6f636b735f73696e63655f6c6173745f73746570584c6173744d656368616e73696d53746570426c6f636b010104069c1820000000000000000004c4202d2d2d204d41502028206e65747569642029202d2d3e206c6173745f6d656368616e69736d5f737465705f626c6f636b2c5375626e65744f776e6572010104069c008000000000000000000000000000000000000000000000000000000000000000000490202d2d2d204d41502028206e65747569642029202d2d3e207375626e65745f6f776e6572445375626e65744f776e6572486f746b6579010104069c0080000000000000000000000000000000000000000000000000000000000000000004ac202d2d2d204d41502028206e65747569642029202d2d3e207375626e65745f6f776e65725f686f746b65794053657276696e67526174654c696d6974010104069c1820320000000000000004a8202d2d2d204d41502028206e65747569642029202d2d3e2073657276696e675f726174655f6c696d69740c52686f010104069c9c080a00046c202d2d2d204d41502028206e65747569642029202d2d3e2052686f144b61707061010104069c9c08ff7f0474202d2d2d204d41502028206e65747569642029202d2d3e204b61707061644e6575726f6e73546f5072756e6541744e65787445706f6368010104069c9c080000042901202d2d2d204d41502028206e65747569642029202d2d3e207569642c2077652075736520746f207265636f7264207569647320746f207072756e65206174206e6578742065706f63682e64526567697374726174696f6e7354686973496e74657276616c010104069c9c08000004cc202d2d2d204d41502028206e65747569642029202d2d3e20726567697374726174696f6e735f746869735f696e74657276616c70504f57526567697374726174696f6e7354686973496e74657276616c010104069c9c08000004dc202d2d2d204d41502028206e65747569642029202d2d3e20706f775f726567697374726174696f6e735f746869735f696e74657276616c744275726e526567697374726174696f6e7354686973496e74657276616c010104069c9c08000004e0202d2d2d204d41502028206e65747569642029202d2d3e206275726e5f726567697374726174696f6e735f746869735f696e74657276616c384d6178416c6c6f77656455696473010104069c9c08001004a0202d2d2d204d41502028206e65747569642029202d2d3e206d61785f616c6c6f7765645f7569647338496d6d756e697479506572696f64010104069c9c080010049c202d2d2d204d41502028206e65747569642029202d2d3e20696d6d756e6974795f706572696f643841637469766974794375746f6666010104069c9c088813049c202d2d2d204d41502028206e65747569642029202d2d3e2061637469766974795f6375746f66663c4d6178576569676874734c696d6974010104069c9c08e80304a0202d2d2d204d41502028206e65747569642029202d2d3e206d61785f7765696768745f6c696d6974445765696768747356657273696f6e4b6579010104069c1820000000000000000004ac202d2d2d204d41502028206e65747569642029202d2d3e20776569676874735f76657273696f6e5f6b6579444d696e416c6c6f77656457656967687473010104069c9c08000404ac202d2d2d204d41502028206e65747569642029202d2d3e206d696e5f616c6c6f7765645f77656967687473504d6178416c6c6f77656456616c696461746f7273010104069c9c08800004b8202d2d2d204d41502028206e65747569642029202d2d3e206d61785f616c6c6f7765645f76616c696461746f72734841646a7573746d656e74496e74657276616c010104069c9c08640004ac202d2d2d204d41502028206e65747569642029202d2d3e2061646a7573746d656e745f696e74657276616c48426f6e64734d6f76696e6741766572616765010104069c1820a0bb0d000000000004b0202d2d2d204d41502028206e65747569642029202d2d3e20626f6e64735f6d6f76696e675f6176657261676530426f6e647350656e616c7479010104069c9c0800000494202d2d2d204d41502028206e65747569642029202d2d3e20626f6e64735f70656e616c74794c57656967687473536574526174654c696d6974010104069c1820640000000000000004b8202d2d2d204d41502028206e65747569642029202d2d3e20776569676874735f7365745f726174655f6c696d69744456616c696461746f725072756e654c656e010104069c1820010000000000000004ac202d2d2d204d41502028206e65747569642029202d2d3e2076616c696461746f725f7072756e655f6c656e3c5363616c696e674c6177506f776572010104069c9c08320004a4202d2d2d204d41502028206e65747569642029202d2d3e207363616c696e675f6c61775f706f77657278546172676574526567697374726174696f6e73506572496e74657276616c010104069c9c08020004e8202d2d2d204d41502028206e65747569642029202d2d3e207461726765745f726567697374726174696f6e735f746869735f696e74657276616c3c41646a7573746d656e74416c706861010104069c1820000000000000000004a0202d2d2d204d41502028206e65747569642029202d2d3e2061646a7573746d656e745f616c70686168436f6d6d697452657665616c57656967687473456e61626c6564010104069c24040004f0202d2d2d204d41502028206e65747569642029202d2d3e20636f6d6d69742072657665616c20763220776569676874732061726520656e61626c6564104275726e010104069c182000ca9a3b000000000470202d2d2d204d41502028206e65747569642029202d2d3e204275726e28446966666963756c7479010104069c182080969800000000000488202d2d2d204d41502028206e65747569642029202d2d3e20446966666963756c74791c4d696e4275726e010104069c182020a1070000000000047c202d2d2d204d41502028206e65747569642029202d2d3e204d696e4275726e1c4d61784275726e010104069c182000e8764817000000047c202d2d2d204d41502028206e65747569642029202d2d3e204d61784275726e344d696e446966666963756c7479010104069c182080969800000000000494202d2d2d204d41502028206e65747569642029202d2d3e204d696e446966666963756c7479344d6178446966666963756c7479010104069c1820ffffffffffffff3f0494202d2d2d204d41502028206e65747569642029202d2d3e204d6178446966666963756c74794c4c61737441646a7573746d656e74426c6f636b010104069c1820000000000000000004c8202d2d2d204d41502028206e65747569642029202d2d3e2020426c6f636b206174206c6173742061646a7573746d656e742e58526567697374726174696f6e7354686973426c6f636b010104069c9c08000004d0202d2d2d204d41502028206e65747569642029202d2d3e20526567697374726174696f6e73206f66207468697320426c6f636b2e6852414f52656379636c6564466f72526567697374726174696f6e010104069c1820000000000000000004f0202d2d2d204d41502028206e65747569642029202d2d3e20676c6f62616c5f52414f5f72656379636c65645f666f725f726567697374726174696f6e2c5478526174654c696d697401001820e803000000000000046c202d2d2d204954454d20282074785f726174655f6c696d697420295c547844656c656761746554616b65526174654c696d697401001820c04b03000000000004a4202d2d2d204954454d20282074785f64656c65676174655f74616b655f726174655f6c696d697420295c54784368696c646b657954616b65526174654c696d697401001820c04b03000000000004a4202d2d2d204954454d20282074785f6368696c646b65795f74616b655f726174655f6c696d69742029344c6971756964416c7068614f6e010104029c24040004f8202d2d2d204d41502028206e65747569642029202d2d3e2057686574686572206f72206e6f74204c697175696420416c70686120697320656e61626c65642c416c70686156616c756573010104069cb9021033b366e604b020204d41502028206e65747569642029202d2d3e2028616c7068615f6c6f772c20616c7068615f68696768293c4e6574776f726b4d61785374616b65010104069c1820ffffffffffffffff04c8204d41502028206e65747569642029202d2d3e206d6178207374616b6520616c6c6f776564206f6e2061207375626e65742e2c5374616b65576569676874010104069cbd0204000ca0203d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3da0203d3d3d3d205375626e6574776f726b20436f6e73656e7375732053746f7261676520203d3d3d3da0203d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d1055696473000108060275029c04000490202d2d2d20444d41502028206e65747569642c20686f746b65792029202d2d3e20756964104b6579730101080606b902008000000000000000000000000000000000000000000000000000000000000000000490202d2d2d20444d41502028206e65747569642c207569642029202d2d3e20686f746b6579384c6f61646564456d697373696f6e000104069cc102040004a0202d2d2d204d41502028206e65747569642029202d2d3e2028686f746b65792c2073652c2076652918416374697665010104069cc90204000478202d2d2d204d41502028206e65747569642029202d2d3e206163746976651052616e6b010104069cbd0204000470202d2d2d204d41502028206e65747569642029202d2d3e2072616e6b145472757374010104069cbd0204000474202d2d2d204d41502028206e65747569642029202d2d3e20747275737424436f6e73656e737573010104069cbd0204000484202d2d2d204d41502028206e65747569642029202d2d3e20636f6e73656e73757324496e63656e74697665010104069cbd0204000484202d2d2d204d41502028206e65747569642029202d2d3e20696e63656e74697665244469766964656e6473010104069cbd0204000484202d2d2d204d41502028206e65747569642029202d2d3e206469766964656e647320456d697373696f6e010104069c510104000480202d2d2d204d41502028206e65747569642029202d2d3e20656d697373696f6e284c617374557064617465010104069c51010400048c202d2d2d204d41502028206e65747569642029202d2d3e206c6173745f7570646174653856616c696461746f725472757374010104069cbd020400049c202d2d2d204d41502028206e65747569642029202d2d3e2076616c696461746f725f7472757374345072756e696e6753636f726573010104069cbd0204000498202d2d2d204d41502028206e65747569642029202d2d3e207072756e696e675f73636f7265733c56616c696461746f725065726d6974010104069cc902040004a0202d2d2d204d41502028206e65747569642029202d2d3e2076616c696461746f725f7065726d69741c576569676874730101080606b902cd0204000494202d2d2d20444d41502028206e65747569642c207569642029202d2d3e207765696768747314426f6e64730101080606b902cd020400048c202d2d2d20444d41502028206e65747569642c207569642029202d2d3e20626f6e64734c426c6f636b4174526567697374726174696f6e0101080606b9021820000000000000000004cc202d2d2d20444d41502028206e65747569642c207569642029202d2d3e20626c6f636b5f61745f726567697374726174696f6e1441786f6e7300010806027502d102040004a4202d2d2d204d41502028206e65747569642c20686f746b65792029202d2d3e2061786f6e5f696e666f484e6575726f6e43657274696669636174657300010806027502d502040004ac202d2d2d204d41502028206e65747569642c20686f746b65792029202d2d3e2063657274696669636174652850726f6d65746865757300010806027502dd02040004bc202d2d2d204d41502028206e65747569642c20686f746b65792029202d2d3e2070726f6d6574686575735f696e666f284964656e7469746965730001040200e102040000304964656e74697469657356320001040200e502040000405375626e65744964656e746974696573000104029ce902040000485375626e65744964656e7469746965735632000104029ced020400005c5472616e73616374696f6e4b65794c617374426c6f636b01010c020606f102182000000000000000000c88203d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d88203d3d3d3d2041786f6e202f2050726f6d6f20456e64706f696e7473203d3d3d3d3d88203d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d2c4c6173745478426c6f636b010104060018200000000000000000047c202d2d2d204d41502028206b65792029202d2d3e206c6173745f626c6f636b5c4c6173745478426c6f636b4368696c644b657954616b6501010406001820000000000000000004c0202d2d2d204d41502028206b65792029202d2d3e206c6173745f74785f626c6f636b5f6368696c646b65795f74616b655c4c6173745478426c6f636b44656c656761746554616b6501010406001820000000000000000004c0202d2d2d204d41502028206b65792029202d2d3e206c6173745f74785f626c6f636b5f64656c65676174655f74616b65385374616b655468726573686f6c640100182000000000000000000468204954454d2820776569676874735f6d696e5f7374616b65202934576569676874436f6d6d69747300010805057502f5020400047902202d2d2d204d415020286e65747569642c2077686f29202d2d3e2056656344657175653c28686173682c20636f6d6d69745f626c6f636b2c2066697273745f72657665616c5f626c6f636b2c206c6173745f72657665616c5f626c6f636b293e207c2053746f7265732061207175657565206f6620636f6d6d69747320666f7220616e206163636f756e74206f6e206120676976656e206e65747569642e4443525633576569676874436f6d6d6974730101080505fd0201030400048102202d2d2d204d415020286e65747569642c20636f6d6d69745f65706f636829202d2d3e2056656344657175653c2877686f2c2073657269616c697a65645f636f6d707265737365645f636f6d6d69742c2072657665616c5f726f756e64293e207c2053746f7265732061207175657565206f6620763320636f6d6d69747320666f7220616e206163636f756e74206f6e206120676976656e206e65747569642e4852657665616c506572696f6445706f636873010104059c18200100000000000000042101202d2d2d204d617020286e657475696429202d2d3e204e756d626572206f662065706f63687320616c6c6f77656420666f7220636f6d6d69742072657665616c20706572696f64733c4861734d6967726174696f6e52756e01010406382404000c4c203d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d4c203d3d3d3d2047656e65736973203d3d3d3d3d4c203d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d010d030198d43c496e697469616c49737375616e6365182000000000000000001088203d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d88203d3d3d3d20496e697469616c2056616c756520436f6e7374616e7473203d3d3d3d88203d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d6c20496e697469616c2063757272656e63792069737375616e63652e60496e697469616c4d696e416c6c6f776564576569676874739c080004049420496e697469616c206d696e20616c6c6f77656420776569676874732073657474696e672e50496e697469616c456d697373696f6e56616c75659c080000046020496e697469616c20456d697373696f6e20526174696f2e58496e697469616c4d6178576569676874734c696d69749c08e803046820496e697469616c206d617820776569676874206c696d69742e30496e697469616c54656d706f9c08630004602054656d706f20666f722065616368206e6574776f726b2e44496e697469616c446966666963756c747918208096980000000000045020496e697469616c20446966666963756c74792e50496e697469616c4d6178446966666963756c74791820ffffffffffffff3f046020496e697469616c204d617820446966666963756c74792e50496e697469616c4d696e446966666963756c747918208096980000000000046020496e697469616c204d696e20446966666963756c74792e84496e697469616c52414f52656379636c6564466f72526567697374726174696f6e18200000000000000000045820496e697469616c2052414f2052656379636c65642e2c496e697469616c4275726e182000ca9a3b00000000043820496e697469616c204275726e2e38496e697469616c4d61784275726e182000e8764817000000044820496e697469616c204d6178204275726e2e38496e697469616c4d696e4275726e182020a1070000000000044820496e697469616c204d696e204275726e2e64496e697469616c41646a7573746d656e74496e74657276616c9c086400047420496e697469616c2061646a7573746d656e7420696e74657276616c2e64496e697469616c426f6e64734d6f76696e67417665726167651820a0bb0d0000000000047820496e697469616c20626f6e6473206d6f76696e6720617665726167652e4c496e697469616c426f6e647350656e616c74799c080000045c20496e697469616c20626f6e64732070656e616c74792e94496e697469616c546172676574526567697374726174696f6e73506572496e74657276616c9c08020004ac20496e697469616c2074617267657420726567697374726174696f6e732070657220696e74657276616c2e28496e697469616c52686f9c080a0004382052686f20636f6e7374616e742e30496e697469616c4b617070619c08ff7f0440204b6170706120636f6e7374616e742e54496e697469616c4d6178416c6c6f776564556964739c0800100448204d61782055494420636f6e7374616e742e60496e697469616c56616c696461746f725072756e654c656e1820010000000000000004a820496e697469616c2076616c696461746f7220636f6e74657874207072756e696e67206c656e6774682e58496e697469616c5363616c696e674c6177506f7765729c083200046c20496e697469616c207363616c696e67206c617720706f7765722e54496e697469616c496d6d756e697479506572696f649c080010046820496d6d756e69747920506572696f6420436f6e7374616e742e54496e697469616c41637469766974794375746f66669c088813044c20416374697669747920636f6e7374616e742e7c496e697469616c4d6178526567697374726174696f6e73506572426c6f636b9c080100049420496e697469616c206d617820726567697374726174696f6e732070657220626c6f636b2e4c496e697469616c5072756e696e6753636f72659c08ffff049c20496e697469616c207072756e696e672073636f726520666f722065616368206e6575726f6e2e6c496e697469616c4d6178416c6c6f77656456616c696461746f72739c08800004c020496e697469616c206d6178696d756d20616c6c6f7765642076616c696461746f727320706572206e6574776f726b2e68496e697469616c44656661756c7444656c656761746554616b659c08142e048420496e697469616c2064656661756c742064656c65676174696f6e2074616b652e58496e697469616c4d696e44656c656761746554616b659c080000048420496e697469616c206d696e696d756d2064656c65676174696f6e2074616b652e68496e697469616c44656661756c744368696c644b657954616b659c080000047c20496e697469616c2064656661756c74206368696c646b65792074616b652e58496e697469616c4d696e4368696c644b657954616b659c080000047c20496e697469616c206d696e696d756d206368696c646b65792074616b652e58496e697469616c4d61784368696c644b657954616b659c08142e047c20496e697469616c206d6178696d756d206368696c646b65792074616b652e60496e697469616c5765696768747356657273696f6e4b657918200000000000000000047420496e697469616c20776569676874732076657273696f6e206b65792e5c496e697469616c53657276696e67526174654c696d697418203200000000000000047020496e697469616c2073657276696e672072617465206c696d69742e48496e697469616c5478526174654c696d69741820e803000000000000048020496e697469616c207472616e73616374696f6e2072617465206c696d69742e78496e697469616c547844656c656761746554616b65526174654c696d69741820c04b03000000000004b820496e697469616c2064656c65676174652074616b65207472616e73616374696f6e2072617465206c696d69742e78496e697469616c54784368696c644b657954616b65526174654c696d69741820c04b03000000000004b820496e697469616c206368696c646b65792074616b65207472616e73616374696f6e2072617465206c696d69742e90496e697469616c53656e61746552657175697265645374616b6550657263656e746167651820010000000000000004ec20496e697469616c2070657263656e74616765206f6620746f74616c207374616b6520726571756972656420746f206a6f696e2073656e6174652e58496e697469616c41646a7573746d656e74416c7068611820000000000000000004a820496e697469616c2061646a7573746d656e7420616c706861206f6e206275726e20616e6420706f772e70496e697469616c4e6574776f726b496d6d756e697479506572696f641820e0c4000000000000048020496e697469616c206e6574776f726b20696d6d756e69747920706572696f6470496e697469616c4e6574776f726b4d696e416c6c6f776564556964739c088000049420496e697469616c206d696e696d756d20616c6c6f776564206e6574776f726b205549447364496e697469616c4e6574776f726b4d696e4c6f636b436f737418200010a5d4e8000000048820496e697469616c206e6574776f726b206d696e696d756d206275726e20636f737454496e697469616c5375626e65744f776e65724375749c08142e047020496e697469616c206e6574776f726b207375626e6574206375742e8c496e697469616c4e6574776f726b4c6f636b526564756374696f6e496e74657276616c1820c089010000000000048420496e697469616c206c6f636b20726564756374696f6e20696e74657276616c2e48496e697469616c5375626e65744c696d69749c080c00047020496e697469616c206d617820616c6c6f776564207375626e6574735c496e697469616c4e6574776f726b526174654c696d69741820201c000000000000049020496e697469616c206e6574776f726b206372656174696f6e2072617465206c696d69742c4b657953776170436f7374182000e1f50500000000046c20436f7374206f66207377617070696e67206120686f746b65792e24416c706861486967689c0866e60401012054686520757070657220626f756e6420666f722074686520616c70686120706172616d657465722e205573656420666f72204c697175696420416c7068612e20416c7068614c6f779c0833b304010120546865206c6f77657220626f756e6420666f722074686520616c70686120706172616d657465722e205573656420666f72204c697175696420416c7068612e344c6971756964416c7068614f6e24040004bc204120666c616720746f20696e646963617465206966204c697175696420416c70686120697320656e61626c65642e58496e697469616c4e6574776f726b4d61785374616b651820ffffffffffffffff046c20496e697469616c206e6574776f726b206d6178207374616b652e88496e697469616c436f6c646b6579537761705363686564756c654475726174696f6e1010a08c0000048020436f6c646b65792073776170207363686564756c65206475617274696f6e2e98496e697469616c446973736f6c76654e6574776f726b5363686564756c654475726174696f6e1010a08c0000048c20446973736f6c7665206e6574776f726b207363686564756c65206475726174696f6e40496e697469616c54616f5765696768741820fc62e03efa3c7c0d045020496e697469616c2054414f207765696768742e010d0607002c547269756d766972617465012c547269756d766972617465182450726f706f73616c7301001106040004902054686520686173686573206f6620746865206163746976652070726f706f73616c732e2850726f706f73616c4f6600010406342503040004cc2041637475616c2070726f706f73616c20666f72206120676976656e20686173682c20696620697427732063757272656e742e18566f74696e6700010406341506040004b420566f746573206f6e206120676976656e2070726f706f73616c2c206966206974206973206f6e676f696e672e3450726f706f73616c436f756e74010010100000000004482050726f706f73616c7320736f206661722e1c4d656d6265727301005d020400043901205468652063757272656e74206d656d62657273206f662074686520636f6c6c6563746976652e20546869732069732073746f72656420736f7274656420286a7573742062792076616c7565292e145072696d65000000040004650120546865207072696d65206d656d62657220746861742068656c70732064657465726d696e65207468652064656661756c7420766f7465206265686176696f7220696e2063617365206f6620616273656e746174696f6e732e01290301c000011906080048547269756d7669726174654d656d626572730148547269756d7669726174654d656d62657273081c4d656d6265727301001d06040004c8205468652063757272656e74206d656d626572736869702c2073746f72656420617320616e206f726465726564205665632e145072696d65000000040004a4205468652063757272656e74207072696d65206d656d6265722c206966206f6e65206578697374732e012d0301c40001210609003453656e6174654d656d62657273013453656e6174654d656d62657273081c4d656d6265727301002506040004c8205468652063757272656e74206d656d626572736869702c2073746f72656420617320616e206f726465726564205665632e145072696d65000000040004a4205468652063757272656e74207072696d65206d656d6265722c206966206f6e65206578697374732e01310301c8000129060a001c5574696c6974790001350301cc044c626174636865645f63616c6c735f6c696d69741010aa2a000004a820546865206c696d6974206f6e20746865206e756d626572206f6620626174636865642063616c6c732e012d060b00105375646f01105375646f040c4b6579000000040004842054686520604163636f756e74496460206f6620746865207375646f206b65792e014d0301d0000131060c00204d756c746973696701204d756c746973696704244d756c746973696773000108050235063906040004942054686520736574206f66206f70656e206d756c7469736967206f7065726174696f6e732e01510301d40c2c4465706f7369744261736518200029de070000000018590120546865206261736520616d6f756e74206f662063757272656e6379206e656564656420746f207265736572766520666f72206372656174696e672061206d756c746973696720657865637574696f6e206f7220746f842073746f726520612064697370617463682063616c6c20666f72206c617465722e00010120546869732069732068656c6420666f7220616e206164646974696f6e616c2073746f72616765206974656d2077686f73652076616c75652073697a652069733101206034202b2073697a656f662828426c6f636b4e756d6265722c2042616c616e63652c204163636f756e74496429296020627974657320616e642077686f7365206b65792073697a652069738020603332202b2073697a656f66284163636f756e74496429602062797465732e344465706f736974466163746f7218200048e801000000000c55012054686520616d6f756e74206f662063757272656e6379206e65656465642070657220756e6974207468726573686f6c64207768656e206372656174696e672061206d756c746973696720657865637574696f6e2e00250120546869732069732068656c6420666f7220616464696e67203332206279746573206d6f726520696e746f2061207072652d6578697374696e672073746f726167652076616c75652e384d61785369676e61746f7269657310106400000004ec20546865206d6178696d756d20616d6f756e74206f66207369676e61746f7269657320616c6c6f77656420696e20746865206d756c74697369672e0141060d0020507265696d6167650120507265696d6167650c24537461747573466f72000104063445060400049020546865207265717565737420737461747573206f66206120676976656e20686173682e4052657175657374537461747573466f72000104063451060400049020546865207265717565737420737461747573206f66206120676976656e20686173682e2c507265696d616765466f72000104066106650604000001590301dc000169060e00245363686564756c657201245363686564756c6572103c496e636f6d706c65746553696e6365000010040000184167656e646101010405106d060400044d01204974656d7320746f2062652065786563757465642c20696e64657865642062792074686520626c6f636b206e756d626572207468617420746865792073686f756c64206265206578656375746564206f6e2e1c5265747269657300010402e48906040004210120526574727920636f6e66696775726174696f6e7320666f72206974656d7320746f2062652065786563757465642c20696e6465786564206279207461736b20616464726573732e184c6f6f6b75700001040504e4040010f8204c6f6f6b75702066726f6d2061206e616d6520746f2074686520626c6f636b206e756d62657220616e6420696e646578206f6620746865207461736b2e00590120466f72207633202d3e207634207468652070726576696f75736c7920756e626f756e646564206964656e7469746965732061726520426c616b65322d3235362068617368656420746f20666f726d2074686520763430206964656e7469746965732e015d0301e008344d6178696d756d5765696768742c400b0000dd0ee90213cccccccccccccccc04290120546865206d6178696d756d207765696768742074686174206d6179206265207363686564756c65642070657220626c6f636b20666f7220616e7920646973706174636861626c65732e504d61785363686564756c6564506572426c6f636b101032000000141d0120546865206d6178696d756d206e756d626572206f66207363686564756c65642063616c6c7320696e2074686520717565756520666f7220612073696e676c6520626c6f636b2e0018204e4f54453a5101202b20446570656e64656e742070616c6c657473272062656e63686d61726b73206d696768742072657175697265206120686967686572206c696d697420666f72207468652073657474696e672e205365742061c420686967686572206c696d697420756e646572206072756e74696d652d62656e63686d61726b736020666561747572652e018d060f001450726f7879011450726f7879081c50726f7869657301010405009106240000000000000000000845012054686520736574206f66206163636f756e742070726f786965732e204d61707320746865206163636f756e74207768696368206861732064656c65676174656420746f20746865206163636f756e7473210120776869636820617265206265696e672064656c65676174656420746f2c20746f67657468657220776974682074686520616d6f756e742068656c64206f6e206465706f7369742e34416e6e6f756e63656d656e74730101040500a1062400000000000000000004ac2054686520616e6e6f756e63656d656e7473206d616465206279207468652070726f787920286b6579292e01650301ec184050726f78794465706f736974426173651820008793030000000010110120546865206261736520616d6f756e74206f662063757272656e6379206e656564656420746f207265736572766520666f72206372656174696e6720612070726f78792e00010120546869732069732068656c6420666f7220616e206164646974696f6e616c2073746f72616765206974656d2077686f73652076616c75652073697a652069732501206073697a656f662842616c616e6365296020627974657320616e642077686f7365206b65792073697a65206973206073697a656f66284163636f756e74496429602062797465732e4850726f78794465706f736974466163746f721820408af7010000000014bc2054686520616d6f756e74206f662063757272656e6379206e6565646564207065722070726f78792061646465642e00350120546869732069732068656c6420666f7220616464696e6720333220627974657320706c757320616e20696e7374616e6365206f66206050726f78795479706560206d6f726520696e746f20616101207072652d6578697374696e672073746f726167652076616c75652e20546875732c207768656e20636f6e6669677572696e67206050726f78794465706f736974466163746f7260206f6e652073686f756c642074616b65f420696e746f206163636f756e7420603332202b2070726f78795f747970652e656e636f646528292e6c656e282960206279746573206f6620646174612e284d617850726f7869657310101400000004f020546865206d6178696d756d20616d6f756e74206f662070726f7869657320616c6c6f77656420666f7220612073696e676c65206163636f756e742e284d617850656e64696e6710104b00000004450120546865206d6178696d756d20616d6f756e74206f662074696d652d64656c6179656420616e6e6f756e63656d656e747320746861742061726520616c6c6f77656420746f2062652070656e64696e672e5c416e6e6f756e63656d656e744465706f736974426173651820005125020000000010310120546865206261736520616d6f756e74206f662063757272656e6379206e656564656420746f207265736572766520666f72206372656174696e6720616e20616e6e6f756e63656d656e742e00490120546869732069732068656c64207768656e2061206e65772073746f72616765206974656d20686f6c64696e672061206042616c616e636560206973206372656174656420287479706963616c6c7920313620206279746573292e64416e6e6f756e63656d656e744465706f736974466163746f72182000990d040000000010d42054686520616d6f756e74206f662063757272656e6379206e65656465642070657220616e6e6f756e63656d656e74206d6164652e00590120546869732069732068656c6420666f7220616464696e6720616e20604163636f756e744964602c2060486173686020616e642060426c6f636b4e756d6265726020287479706963616c6c79203638206279746573298c20696e746f2061207072652d6578697374696e672073746f726167652076616c75652e01b10610002052656769737472790120526567697374727904284964656e746974794f660001040500b50604000464204964656e746974792064617461206279206163636f756e74016d0301f40c4c4d61784164646974696f6e616c4669656c6473101001000000085420436f6e66696775726174696f6e206669656c6473a8204d6178696d756d20757365722d636f6e66696775726564206164646974696f6e616c206669656c647338496e697469616c4465706f736974182000e1f5050000000004d42054686520616d6f756e742068656c64206f6e206465706f73697420666f7220612072656769737465726564206964656e74697479304669656c644465706f736974182000e1f50500000000042d012054686520616d6f756e742068656c64206f6e206465706f73697420706572206164646974696f6e616c206669656c6420666f7220612072656769737465726564206964656e746974792e01b90611002c436f6d6d69746d656e7473012c436f6d6d69746d656e74730c24526174654c696d69740100101064000000047c205468652072617465206c696d697420666f7220636f6d6d69746d656e747330436f6d6d69746d656e744f6600010806057502bd0604000464204964656e746974792064617461206279206163636f756e74384c617374436f6d6d69746d656e74000108060575021004000001790401f810244d61784669656c647310100100000004290120546865206d6178696d756d206e756d626572206f66206164646974696f6e616c206669656c647320746861742063616e20626520616464656420746f206120636f6d6d69746d656e7438496e697469616c4465706f7369741820000000000000000004d42054686520616d6f756e742068656c64206f6e206465706f73697420666f7220612072656769737465726564206964656e74697479304669656c644465706f73697418200000000000000000042d012054686520616d6f756e742068656c64206f6e206465706f73697420706572206164646974696f6e616c206669656c6420666f7220612072656769737465726564206964656e746974792e4044656661756c74526174654c696d6974101064000000047c205468652072617465206c696d697420666f7220636f6d6d69746d656e747301c10612002841646d696e5574696c7300018d0501fc0001c506130020536166654d6f64650120536166654d6f64650830456e7465726564556e74696c000010040014290120436f6e7461696e7320746865206c61737420626c6f636b206e756d62657220746861742074686520736166652d6d6f64652077696c6c2072656d61696e20656e746572656420696e2e00a4202053657420746f20604e6f6e6560207768656e20736166652d6d6f6465206973206578697465642e00510120536166652d6d6f6465206973206175746f6d61746963616c6c7920657869746564207768656e207468652063757272656e7420626c6f636b206e756d626572206578636565647320746869732076616c75652e204465706f736974730001080505c90618040010350120486f6c64732074686520726573657276652074686174207761732074616b656e2066726f6d20616e206163636f756e74206174206120737065636966696320626c6f636b206e756d6265722e00750120546869732068656c707320676f7665726e616e636520746f206861766520616e206f76657276696577206f66206f75747374616e64696e67206465706f7369747320746861742073686f756c642062652072657475726e6564206f722420736c61736865642e0191050101011434456e7465724475726174696f6e10100000000004210120466f7220686f77206d616e7920626c6f636b732074686520736166652d6d6f64652077696c6c20626520656e7465726564206279205b6050616c6c65743a3a656e746572605d2e38457874656e644475726174696f6e1010000000000c4d0120466f7220686f77206d616e7920626c6f636b732074686520736166652d6d6f64652063616e20626520657874656e6465642062792065616368205b6050616c6c65743a3a657874656e64605d2063616c6c2e004d01205468697320646f6573206e6f7420696d706f736520612068617264206c696d69742061732074686520736166652d6d6f64652063616e20626520657874656e646564206d756c7469706c652074696d65732e48456e7465724465706f736974416d6f756e74cd0604000c05012054686520616d6f756e7420746861742077696c6c2062652072657365727665642075706f6e2063616c6c696e67205b6050616c6c65743a3a656e746572605d2e00410120604e6f6e656020646973616c6c6f7773207065726d697373696f6e6c6573736c7920656e61626c696e672074686520736166652d6d6f646520616e6420697320612073616e652064656661756c742e4c457874656e644465706f736974416d6f756e74cd0604000c09012054686520616d6f756e7420746861742077696c6c2062652072657365727665642075706f6e2063616c6c696e67205b6050616c6c65743a3a657874656e64605d2e00450120604e6f6e656020646973616c6c6f7773207065726d697373696f6e6c6573736c7920657874656e64696e672074686520736166652d6d6f646520616e6420697320612073616e652064656661756c742e3052656c6561736544656c6179d101040020490120546865206d696e696d616c206475726174696f6e2061206465706f7369742077696c6c2072656d61696e20726573657276656420616674657220736166652d6d6f646520697320656e7465726564206f72490120657874656e6465642c20756e6c657373205b6050616c6c65743a3a666f7263655f72656c656173655f6465706f736974605d206973207375636365737366756c6c792063616c6c656420736f6f6e65722e005901204576657279206465706f736974206973207469656420746f20612073706563696669632061637469766174696f6e206f7220657874656e73696f6e2c20746875732065616368206465706f7369742063616e206265e82072656c656173656420696e646570656e64656e746c79206166746572207468652064656c617920666f7220697420686173207061737365642e00450120604e6f6e656020646973616c6c6f7773207065726d697373696f6e6c6573736c792072656c656173696e672074686520736166652d6d6f6465206465706f7369747320616e6420697320612073616e65242064656661756c742e01d106140020457468657265756d0120457468657265756d141c50656e64696e670100d506040004d02043757272656e74206275696c64696e6720626c6f636b2773207472616e73616374696f6e7320616e642072656365697074732e3043757272656e74426c6f636b0000f90604000470205468652063757272656e7420457468657265756d20626c6f636b2e3c43757272656e74526563656970747300000d070400047c205468652063757272656e7420457468657265756d2072656365697074732e6843757272656e745472616e73616374696f6e53746174757365730000110704000488205468652063757272656e74207472616e73616374696f6e2073746174757365732e24426c6f636b4861736801010405410134800000000000000000000000000000000000000000000000000000000000000000000195050109010001150715000c45564d010c45564d18304163636f756e74436f646573010104020d0138040000504163636f756e74436f6465734d65746164617461000104020d0119070400003c4163636f756e7453746f726167657301010802021d073480000000000000000000000000000000000000000000000000000000000000000000205375696369646564000104020d01a40400004c57686974656c697374656443726561746f72730100cd050400005444697361626c6557686974656c697374436865636b01002404000001bd050135010001210716002845564d436861696e4964012845564d436861696e4964041c436861696e49640100182000000000000000000448205468652045564d20636861696e2049442e0000000017001c42617365466565011c42617365466565083442617365466565506572476173010041018000c817a8040000000000000000000000000000000000000000000000000000000028456c6173746963697479010049011048e801000001d105013d0100001900144472616e6401144472616e641030426561636f6e436f6e6669670100fd053903810183cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a030000002721e6648052db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e97180f477d5c89f21a17c863a7f937c6a6d15859414d2be09cd448d4279af331c5d3e60626c732d756e636861696e65642d67312d7266633933383020717569636b6e6574047c20746865206472616e6420626561636f6e20636f6e66696775726174696f6e1850756c7365730001040218e50504000468206d617020726f756e64206e756d62657220746f2070756c73653c4c61737453746f726564526f756e6401001820000000000000000000384e657874556e7369676e656441740100101000000000140d0120446566696e65732074686520626c6f636b207768656e206e65787420756e7369676e6564207472616e73616374696f6e2077696c6c2062652061636365707465642e001d0120546f2070726576656e74207370616d206f6620756e7369676e65642028616e6420756e706169642129207472616e73616374696f6e73206f6e20746865206e6574776f726b2ca4207765206f6e6c7920616c6c6f77206f6e65207472616e73616374696f6e2070657220626c6f636b2e250120546869732073746f7261676520656e74727920646566696e6573207768656e206e6577207472616e73616374696f6e20697320676f696e6720746f2062652061636365707465642e01d505014d010840556e7369676e65645072696f726974791820000010000000000010f0204120636f6e66696775726174696f6e20666f722062617365207072696f72697479206f6620756e7369676e6564207472616e73616374696f6e732e0015012054686973206973206578706f73656420736f20746861742069742063616e2062652074756e656420666f7220706172746963756c61722072756e74696d652c207768656eb4206d756c7469706c652070616c6c6574732073656e6420756e7369676e6564207472616e73616374696f6e732e4048747470466574636854696d656f75741820e80300000000000008490120546865206d6178696d756d206e756d626572206f66206d696c6c697365636f6e6473207765206172652077696c6c696e6720746f207761697420666f72207468652048545450207265717565737420746f2820636f6d706c6574652e0125071a000455022503f50529072c48436865636b4e6f6e5a65726f53656e6465722d07a440436865636b5370656356657273696f6e31071038436865636b547856657273696f6e35071030436865636b47656e6573697339073438436865636b4d6f7274616c6974793d073428436865636b4e6f6e63654507a42c436865636b5765696768744907a4604368617267655472616e73616374696f6e5061796d656e744d07a46053756274656e736f725369676e6564457874656e73696f6e5107a468436f6d6d69746d656e74735369676e6564457874656e73696f6e5907a444436865636b4d65746164617461486173685d07e855074c10436f72650c1c76657273696f6e00950104902052657475726e73207468652076657273696f6e206f66207468652072756e74696d652e34657865637574655f626c6f636b0414626c6f636b6507a4046420457865637574652074686520676976656e20626c6f636b2e40696e697469616c697a655f626c6f636b04186865616465726907790704410120496e697469616c697a65206120626c6f636b20776974682074686520676976656e2068656164657220616e642072657475726e207468652072756e74696d6520657865637574697665206d6f64652e042101205468652060436f7265602072756e74696d65206170692074686174206576657279205375627374726174652072756e74696d65206e6565647320746f20696d706c656d656e742e204d657461646174610c206d65746164617461007d07048c2052657475726e7320746865206d65746164617461206f6620612072756e74696d652e4c6d657461646174615f61745f76657273696f6e041c76657273696f6e10810710a42052657475726e7320746865206d65746164617461206174206120676976656e2076657273696f6e2e0005012049662074686520676976656e206076657273696f6e602069736e277420737570706f727465642c20746869732077696c6c2072657475726e20604e6f6e65602e750120557365205b6053656c663a3a6d657461646174615f76657273696f6e73605d20746f2066696e64206f75742061626f757420737570706f72746564206d657461646174612076657273696f6e206f66207468652072756e74696d652e446d657461646174615f76657273696f6e730085070ca42052657475726e732074686520737570706f72746564206d657461646174612076657273696f6e732e00c020546869732063616e206265207573656420746f2063616c6c20606d657461646174615f61745f76657273696f6e602e0401012054686520604d65746164617461602061706920747261697420746861742072657475726e73206d6574616461746120666f72207468652072756e74696d652e30426c6f636b4275696c646572103c6170706c795f65787472696e736963042465787472696e7369636d078907106c204170706c792074686520676976656e2065787472696e7369632e0039012052657475726e7320616e20696e636c7573696f6e206f7574636f6d652077686963682073706563696669657320696620746869732065787472696e73696320697320696e636c7564656420696e4c207468697320626c6f636b206f72206e6f742e3866696e616c697a655f626c6f636b00690704682046696e697368207468652063757272656e7420626c6f636b2e4c696e686572656e745f65787472696e736963730420696e686572656e7499077507043d012047656e657261746520696e686572656e742065787472696e736963732e2054686520696e686572656e7420646174612077696c6c20766172792066726f6d20636861696e20746f20636861696e2e3c636865636b5f696e686572656e74730814626c6f636b650710646174619907a90704550120436865636b20746861742074686520696e686572656e7473206172652076616c69642e2054686520696e686572656e7420646174612077696c6c20766172792066726f6d20636861696e20746f20636861696e2e047101205468652060426c6f636b4275696c646572602061706920747261697420746861742070726f7669646573207468652072657175697265642066756e6374696f6e616c69747920666f72206275696c64696e67206120626c6f636b2e3847656e657369734275696c6465720c2c6275696c645f737461746504106a736f6e38ad07246501204275696c64206052756e74696d6547656e65736973436f6e666967602066726f6d2061204a534f4e20626c6f62206e6f74207573696e6720616e792064656661756c747320616e642073746f726520697420696e20746865242073746f726167652e00f90120496e207468652063617365206f662061204652414d452d62617365642072756e74696d652c20746869732066756e6374696f6e20646573657269616c697a6573207468652066756c6c206052756e74696d6547656e65736973436f6e666967602066726f6d2074686520676976656e204a534f4e20626c6f6220616e645901207075747320697420696e746f207468652073746f726167652e204966207468652070726f7669646564204a534f4e20626c6f6220697320696e636f7272656374206f7220696e636f6d706c657465206f7220746865b420646573657269616c697a6174696f6e206661696c732c20616e206572726f722069732072657475726e65642e005d0120506c65617365206e6f746520746861742070726f7669646564204a534f4e20626c6f62206d75737420636f6e7461696e20616c6c206052756e74696d6547656e65736973436f6e66696760206669656c64732c206e6f5c2064656661756c74732077696c6c20626520757365642e286765745f70726573657404086964b107b5073861012052657475726e732061204a534f4e20626c6f6220726570726573656e746174696f6e206f6620746865206275696c742d696e206052756e74696d6547656e65736973436f6e66696760206964656e7469666965642062791820606964602e003d01204966206069646020697320604e6f6e6560207468652066756e6374696f6e2072657475726e73204a534f4e20626c6f6220726570726573656e746174696f6e206f66207468652064656661756c744901206052756e74696d6547656e65736973436f6e6669676020737472756374206f66207468652072756e74696d652e20496d706c656d656e746174696f6e206d7573742070726f766964652064656661756c7460206052756e74696d6547656e65736973436f6e666967602e002101204f74686572776973652066756e6374696f6e2072657475726e732061204a534f4e20726570726573656e746174696f6e206f6620746865206275696c742d696e2c206e616d65645101206052756e74696d6547656e65736973436f6e6669676020707265736574206964656e74696669656420627920606964602c206f7220604e6f6e656020696620737563682070726573657420646f6573206e6f7461012065786973742e2052657475726e656420605665633c75383e6020636f6e7461696e73206279746573206f66204a534f4e20626c6f62202870617463682920776869636820636f6d7072697365732061206c697374206f664d012028706f74656e7469616c6c79206e657374656429206b65792d76616c756520706169727320746861742061726520696e74656e64656420666f7220637573746f6d697a696e67207468652064656661756c7465012072756e74696d652067656e6573697320636f6e6669672e20546865207061746368207368616c6c206265206d657267656420287266633733383629207769746820746865204a534f4e20726570726573656e746174696f6e6101206f66207468652064656661756c74206052756e74696d6547656e65736973436f6e6669676020746f20637265617465206120636f6d70726568656e736976652067656e6573697320636f6e66696720746861742063616e84206265207573656420696e20606275696c645f737461746560206d6574686f642e307072657365745f6e616d657300b9071051012052657475726e732061206c697374206f66206964656e7469666965727320666f7220617661696c61626c65206275696c74696e206052756e74696d6547656e65736973436f6e6669676020707265736574732e0061012054686520707265736574732066726f6d20746865206c6973742063616e20626520717565726965642077697468205b6047656e657369734275696c6465723a3a6765745f707265736574605d206d6574686f642e2049660101206e6f206e616d65642070726573657473206172652070726f7669646564206279207468652072756e74696d6520746865206c69737420697320656d7074792e04e82041504920746f20696e74657261637420776974682052756e74696d6547656e65736973436f6e66696720666f72207468652072756e74696d65585461676765645472616e73616374696f6e5175657565045076616c69646174655f7472616e73616374696f6e0c18736f75726365bd070874786d0728626c6f636b5f6861736834c10724682056616c696461746520746865207472616e73616374696f6e2e0065012054686973206d6574686f6420697320696e766f6b656420627920746865207472616e73616374696f6e20706f6f6c20746f206c6561726e2064657461696c732061626f757420676976656e207472616e73616374696f6e2e45012054686520696d706c656d656e746174696f6e2073686f756c64206d616b65207375726520746f207665726966792074686520636f72726563746e657373206f6620746865207472616e73616374696f6e4d0120616761696e73742063757272656e742073746174652e2054686520676976656e2060626c6f636b5f686173686020636f72726573706f6e647320746f207468652068617368206f662074686520626c6f636b7c207468617420697320757365642061732063757272656e742073746174652e004501204e6f7465207468617420746869732063616c6c206d617920626520706572666f726d65642062792074686520706f6f6c206d756c7469706c652074696d657320616e64207472616e73616374696f6e73a4206d6967687420626520766572696669656420696e20616e7920706f737369626c65206f726465722e044d012054686520605461676765645472616e73616374696f6e5175657565602061706920747261697420666f7220696e746572666572696e67207769746820746865207472616e73616374696f6e2071756575652e444f6666636861696e576f726b6572417069043c6f6666636861696e5f776f726b657204186865616465726907a404c82053746172747320746865206f66662d636861696e207461736b20666f7220676976656e20626c6f636b206865616465722e046420546865206f6666636861696e20776f726b6572206170692e1c417572614170690834736c6f745f6475726174696f6e00c9070c902052657475726e732074686520736c6f74206475726174696f6e20666f7220417572612e0025012043757272656e746c792c206f6e6c79207468652076616c75652070726f7669646564206279207468697320747970652061742067656e657369732077696c6c20626520757365642e2c617574686f72697469657300bd01049c2052657475726e207468652063757272656e7420736574206f6620617574686f7269746965732e04b820415049206e656365737361727920666f7220626c6f636b20617574686f7273686970207769746820617572612e2c53657373696f6e4b657973085467656e65726174655f73657373696f6e5f6b657973041073656564b507381c15012047656e6572617465206120736574206f662073657373696f6e206b6579732077697468206f7074696f6e616c6c79207573696e672074686520676976656e20736565642e090120546865206b6579732073686f756c642062652073746f7265642077697468696e20746865206b657973746f7265206578706f736564207669612072756e74696d653c2065787465726e616c69746965732e00b0205468652073656564206e6565647320746f20626520612076616c69642060757466386020737472696e672e00d02052657475726e732074686520636f6e636174656e61746564205343414c4520656e636f646564207075626c6963206b6579732e4c6465636f64655f73657373696f6e5f6b657973041c656e636f64656438cd070c98204465636f64652074686520676976656e207075626c69632073657373696f6e206b6579732e00dc2052657475726e7320746865206c697374206f66207075626c696320726177207075626c6963206b657973202b206b657920747970652e04682053657373696f6e206b6579732072756e74696d65206170692e284772616e647061417069104c6772616e6470615f617574686f7269746965730080183d0120476574207468652063757272656e74204752414e44504120617574686f72697469657320616e6420776569676874732e20546869732073686f756c64206e6f74206368616e6765206578636570741d0120666f72207768656e206368616e67657320617265207363686564756c656420616e642074686520636f72726573706f6e64696e672064656c617920686173207061737365642e003501205768656e2063616c6c656420617420626c6f636b20422c2069742077696c6c2072657475726e2074686520736574206f6620617574686f72697469657320746861742073686f756c642062653d01207573656420746f2066696e616c697a652064657363656e64616e7473206f66207468697320626c6f636b2028422b312c20422b322c202e2e2e292e2054686520626c6f636b204220697473656c66c02069732066696e616c697a65642062792074686520617574686f7269746965732066726f6d20626c6f636b20422d312eb47375626d69745f7265706f72745f65717569766f636174696f6e5f756e7369676e65645f65787472696e736963084865717569766f636174696f6e5f70726f6f66d9013c6b65795f6f776e65725f70726f6f66dd07e107201101205375626d69747320616e20756e7369676e65642065787472696e73696320746f207265706f727420616e2065717569766f636174696f6e2e205468652063616c6c6572f8206d7573742070726f76696465207468652065717569766f636174696f6e2070726f6f6620616e642061206b6579206f776e6572736869702070726f6f66fc202873686f756c64206265206f627461696e6564207573696e67206067656e65726174655f6b65795f6f776e6572736869705f70726f6f6660292e2054686505012065787472696e7369632077696c6c20626520756e7369676e656420616e642073686f756c64206f6e6c7920626520616363657074656420666f72206c6f63616c150120617574686f727368697020286e6f7420746f2062652062726f61646361737420746f20746865206e6574776f726b292e2054686973206d6574686f642072657475726e73090120604e6f6e6560207768656e206372656174696f6e206f66207468652065787472696e736963206661696c732c20652e672e2069662065717569766f636174696f6e0501207265706f7274696e672069732064697361626c656420666f722074686520676976656e2072756e74696d652028692e652e2074686973206d6574686f6420697305012068617264636f64656420746f2072657475726e20604e6f6e6560292e204f6e6c792075736566756c20696e20616e206f6666636861696e20636f6e746578742e7067656e65726174655f6b65795f6f776e6572736869705f70726f6f6608187365745f69641830617574686f726974795f696488e5072c09012047656e65726174657320612070726f6f66206f66206b6579206f776e65727368697020666f722074686520676976656e20617574686f7269747920696e20746865fc20676976656e207365742e20416e206578616d706c65207573616765206f662074686973206d6f64756c6520697320636f75706c656420776974682074686505012073657373696f6e20686973746f726963616c206d6f64756c6520746f2070726f76652074686174206120676976656e20617574686f72697479206b65792069730d01207469656420746f206120676976656e207374616b696e67206964656e7469747920647572696e6720612073706563696669632073657373696f6e2e2050726f6f66731101206f66206b6579206f776e65727368697020617265206e656365737361727920666f72207375626d697474696e672065717569766f636174696f6e207265706f7274732e1101204e4f54453a206576656e2074686f75676820746865204150492074616b6573206120607365745f69646020617320706172616d65746572207468652063757272656e74fc20696d706c656d656e746174696f6e732069676e6f7265207468697320706172616d6574657220616e6420696e73746561642072656c79206f6e20746869730d01206d6574686f64206265696e672063616c6c65642061742074686520636f727265637420626c6f636b206865696768742c20692e652e20616e7920706f696e7420617415012077686963682074686520676976656e20736574206964206973206c697665206f6e2d636861696e2e2046757475726520696d706c656d656e746174696f6e732077696c6c0d0120696e73746561642075736520696e64657865642064617461207468726f75676820616e206f6666636861696e20776f726b65722c206e6f7420726571756972696e6778206f6c6465722073746174657320746f20626520617661696c61626c652e3863757272656e745f7365745f696400180498204765742063757272656e74204752414e44504120617574686f72697479207365742069642e240101204150497320666f7220696e746567726174696e6720746865204752414e4450412066696e616c6974792067616467657420696e746f2072756e74696d65732ec020546869732073686f756c6420626520696d706c656d656e746564206f6e207468652072756e74696d6520736964652e0015012054686973206973207072696d6172696c79207573656420666f72206e65676f74696174696e6720617574686f726974792d736574206368616e67657320666f72207468650d01206761646765742e204752414e44504120757365732061207369676e616c696e67206d6f64656c206f66206368616e67696e6720617574686f7269747920736574733a3101206368616e6765732073686f756c64206265207369676e616c6564207769746820612064656c6179206f66204e20626c6f636b732c20616e64207468656e206175746f6d61746963616c6c79e4206170706c69656420696e207468652072756e74696d652061667465722074686f7365204e20626c6f636b732068617665207061737365642e00fc2054686520636f6e73656e7375732070726f746f636f6c2077696c6c20636f6f7264696e617465207468652068616e646f66662065787465726e616c6c792e3c4163636f756e744e6f6e636541706904346163636f756e745f6e6f6e6365041c6163636f756e74001004c0204765742063757272656e74206163636f756e74206e6f6e6365206f6620676976656e20604163636f756e744964602e0480205468652041504920746f207175657279206163636f756e74206e6f6e63652e545472616e73616374696f6e5061796d656e74417069102871756572795f696e666f080c7578746d070c6c656e10e907004471756572795f6665655f64657461696c73080c7578746d070c6c656e10ed07004c71756572795f7765696768745f746f5f66656504187765696768742c18004c71756572795f6c656e6774685f746f5f66656504186c656e67746810180000645472616e73616374696f6e5061796d656e7443616c6c417069103c71756572795f63616c6c5f696e666f081063616c6c25030c6c656e10e90704490120517565727920696e666f726d6174696f6e206f66206120646973706174636820636c6173732c207765696768742c20616e6420666565206f66206120676976656e20656e636f646564206043616c6c602e5871756572795f63616c6c5f6665655f64657461696c73081063616c6c25030c6c656e10ed0704b4205175657279206665652064657461696c73206f66206120676976656e20656e636f646564206043616c6c602e4c71756572795f7765696768745f746f5f66656504187765696768742c1804010120517565727920746865206f7574707574206f66207468652063757272656e742060576569676874546f4665656020676976656e20736f6d6520696e7075742e4c71756572795f6c656e6774685f746f5f66656504186c656e677468101804010120517565727920746865206f7574707574206f66207468652063757272656e7420604c656e677468546f4665656020676976656e20736f6d6520696e7075742e0054457468657265756d52756e74696d655250434170694420636861696e5f6964001804b42052657475726e732072756e74696d6520646566696e65642070616c6c65745f65766d3a3a436861696e49642e346163636f756e745f6261736963041c616464726573730d01f90704a42052657475726e732070616c6c65745f65766d3a3a4163636f756e747320627920616464726573732e246761735f707269636500410104942052657475726e7320466978656447617350726963653a3a6d696e5f6761735f70726963653c6163636f756e745f636f64655f6174041c616464726573730d013804fc20466f72206120676976656e206163636f756e7420616464726573732c2072657475726e732070616c6c65745f65766d3a3a4163636f756e74436f6465732e18617574686f72000d0104f02052657475726e732074686520636f6e7665727465642046696e64417574686f723a3a66696e645f617574686f7220617574686f726974792069642e2873746f726167655f6174081c616464726573730d0114696e64657841013404310120466f72206120676976656e206163636f756e74206164647265737320616e6420696e6465782c2072657475726e732070616c6c65745f65766d3a3a4163636f756e7453746f72616765732e1063616c6c281066726f6d0d0108746f0d011064617461381476616c75654101246761735f6c696d697441013c6d61785f6665655f7065725f676173c105606d61785f7072696f726974795f6665655f7065725f676173c105146e6f6e6365c10520657374696d617465242c6163636573735f6c697374fd0701080018637265617465241066726f6d0d011064617461381476616c75654101246761735f6c696d697441013c6d61785f6665655f7065725f676173c105606d61785f7072696f726974795f6665655f7065725f676173c105146e6f6e6365c10520657374696d617465242c6163636573735f6c697374fd071508003463757272656e745f626c6f636b001d0804682052657475726e207468652063757272656e7420626c6f636b2e4063757272656e745f726563656970747300210804702052657475726e207468652063757272656e7420726563656970742e7063757272656e745f7472616e73616374696f6e5f7374617475736573002508049c2052657475726e207468652063757272656e74207472616e73616374696f6e207374617475732e2c63757272656e745f616c6c002908004065787472696e7369635f66696c746572040c78747375070507043501205265636569766573206120605665633c4f706171756545787472696e7369633e6020616e642066696c7465727320616c6c2074686520657468657265756d207472616e73616374696f6e732e28656c6173746963697479002d0804882052657475726e2074686520656c6173746963697479206d756c7469706c6965722e706761735f6c696d69745f6d756c7469706c6965725f737570706f727400a4087501205573656420746f2064657465726d696e6520696620676173206c696d6974206d756c7469706c69657220666f72206e6f6e2d7472616e73616374696f6e616c2063616c6c7320286574685f63616c6c2f657374696d617465476173293820697320737570706f727465642e3470656e64696e675f626c6f636b040c7874737507310804682052657475726e207468652070656e64696e6720626c6f636b2e60696e697469616c697a655f70656e64696e675f626c6f636b04186865616465726907a4147820496e697469616c697a65207468652070656e64696e6720626c6f636b2e350120546865206265686176696f722073686f756c64206265207468652073616d65206173207468652072756e74696d652061706920436f72655f696e697469616c697a655f626c6f636b206275745c20666f722061202270656e64696e672220626c6f636b2e610120496620796f75722070726f6a65637420646f6e2774206e65656420746f2068617665206120646966666572656e74206265686176696f7220746f20696e697469616c697a65202270656e64696e672220626c6f636b732ce020796f752063616e20636f707920796f757220436f72655f696e697469616c697a655f626c6f636b20696d706c656d656e746174696f6e2e04c020415049206e656365737361727920666f7220457468657265756d2d636f6d7061746962696c697479206c617965722e70436f6e766572745472616e73616374696f6e52756e74696d65417069044c636f6e766572745f7472616e73616374696f6e042c7472616e73616374696f6e99056d0700005844656c6567617465496e666f52756e74696d654170690c346765745f64656c65676174657300350800306765745f64656c6567617465044064656c65676174655f6163636f756e7400450800346765745f64656c656761746564044464656c6567617465655f6163636f756e740049080000504e6575726f6e496e666f52756e74696d65417069102c6765745f6e6575726f6e7304186e65747569649c510800286765745f6e6575726f6e08186e65747569649c0c7569649c590800406765745f6e6575726f6e735f6c69746504186e65747569649c5d08003c6765745f6e6575726f6e5f6c69746508186e65747569649c0c7569649c65080000505375626e6574496e666f52756e74696d65417069283c6765745f7375626e65745f696e666f04186e65747569649c690800406765745f7375626e6574735f696e666f00790800486765745f7375626e65745f696e666f5f763204186e65747569649c7d08004c6765745f7375626e6574735f696e666f5f763200850800586765745f7375626e65745f6879706572706172616d7304186e65747569649c890800506765745f616c6c5f64796e616d69635f696e666f00910800486765745f616c6c5f6d65746167726170687300a90800346765745f6d657461677261706804186e65747569649cad0800406765745f64796e616d69635f696e666f04186e65747569649c950800406765745f7375626e65745f737461746504186e65747569649cc10800004c5374616b65496e666f52756e74696d654170690c686765745f7374616b655f696e666f5f666f725f636f6c646b6579043c636f6c646b65795f6163636f756e7400cd08006c6765745f7374616b655f696e666f5f666f725f636f6c646b6579730440636f6c646b65795f6163636f756e74735d02d50800a06765745f7374616b655f696e666f5f666f725f686f746b65795f636f6c646b65795f6e65747569640c38686f746b65795f6163636f756e74003c636f6c646b65795f6163636f756e7400186e65747569649cdd080000705375626e6574526567697374726174696f6e52756e74696d6541706904746765745f6e6574776f726b5f726567697374726174696f6e5f636f737400180000250354e10800 \ No newline at end of file diff --git a/tests/helpers/integration_websocket_data.py b/tests/helpers/integration_websocket_data.py new file mode 100644 index 0000000000..41b4f64d1f --- /dev/null +++ b/tests/helpers/integration_websocket_data.py @@ -0,0 +1,9255 @@ +# This is dictionary of raw websocket send vs recv data for given methods used for integration testing, essentially +# stubbing the network I/O only +# This is basically a JSON file, but is not a JSON file because of the discrepancies between JSON and Python types + +from itertools import cycle + + +WEBSOCKET_RESPONSES = { + "blocks_since_last_update": { + "chain_getHead": { + "[]": { + "jsonrpc": "2.0", + "result": "0x9259b771b0f59b1ed8f38f604ab4f4a3e63dc0988754e664be2d40d58970958c", + } + }, + "chain_getHeader": { + '["0x9259b771b0f59b1ed8f38f604ab4f4a3e63dc0988754e664be2d40d58970958c"]': { + "jsonrpc": "2.0", + "result": { + "digest": { + "logs": [ + "0x0661757261209f489a0800000000", + "0x0466726f6e88010f7ec3156c6eaf684b45b80ad326380d363b3ad142e791211a3c500bf0e8799d00", + "0x05617572610101009efb66f8455cc1268daee2bff8babbb168ea54546cd94f9223bdc62725f957054b01b7bd962b24fd8cbbaeb920056ce752d92a6354290e7846e79a8ac4fa89", + ] + }, + "extrinsicsRoot": "0x030274ac1ea87c190cf1e91061cb205622dcfea26abef58a905e4bebdf8ba77c", + "number": "0x31ce92", + "parentHash": "0x119e135f8ae6eac1f3fc47fc14838afcfdbee13e8dba14657ae150f4d56f6df2", + "stateRoot": "0x12d887d9ad0d0468e073db0496a6a173cfd8d27e32324bc670cbbb937792e484", + }, + }, + "[null]": { + "jsonrpc": "2.0", + "result": { + "digest": { + "logs": [ + "0x0661757261209f489a0800000000", + "0x0466726f6e88010f7ec3156c6eaf684b45b80ad326380d363b3ad142e791211a3c500bf0e8799d00", + "0x05617572610101009efb66f8455cc1268daee2bff8babbb168ea54546cd94f9223bdc62725f957054b01b7bd962b24fd8cbbaeb920056ce752d92a6354290e7846e79a8ac4fa89", + ] + }, + "extrinsicsRoot": "0x030274ac1ea87c190cf1e91061cb205622dcfea26abef58a905e4bebdf8ba77c", + "number": "0x31ce92", + "parentHash": "0x119e135f8ae6eac1f3fc47fc14838afcfdbee13e8dba14657ae150f4d56f6df2", + "stateRoot": "0x12d887d9ad0d0468e073db0496a6a173cfd8d27e32324bc670cbbb937792e484", + }, + }, + }, + "rpc_methods": { + "[]": { + "jsonrpc": "2.0", + "result": { + "methods": [ + "account_nextIndex", + "archive_unstable_body", + "archive_unstable_call", + "archive_unstable_finalizedHeight", + "archive_unstable_genesisHash", + "archive_unstable_hashByHeight", + "archive_unstable_header", + "archive_unstable_storage", + "author_hasKey", + "author_hasSessionKeys", + "author_insertKey", + "author_pendingExtrinsics", + "author_removeExtrinsic", + "author_rotateKeys", + "author_submitAndWatchExtrinsic", + "author_submitExtrinsic", + "author_unwatchExtrinsic", + "chainHead_v1_body", + "chainHead_v1_call", + "chainHead_v1_continue", + "chainHead_v1_follow", + "chainHead_v1_header", + "chainHead_v1_stopOperation", + "chainHead_v1_storage", + "chainHead_v1_unfollow", + "chainHead_v1_unpin", + "chainSpec_v1_chainName", + "chainSpec_v1_genesisHash", + "chainSpec_v1_properties", + "chain_getBlock", + "chain_getBlockHash", + "chain_getFinalisedHead", + "chain_getFinalizedHead", + "chain_getHead", + "chain_getHeader", + "chain_getRuntimeVersion", + "chain_subscribeAllHeads", + "chain_subscribeFinalisedHeads", + "chain_subscribeFinalizedHeads", + "chain_subscribeNewHead", + "chain_subscribeNewHeads", + "chain_subscribeRuntimeVersion", + "chain_unsubscribeAllHeads", + "chain_unsubscribeFinalisedHeads", + "chain_unsubscribeFinalizedHeads", + "chain_unsubscribeNewHead", + "chain_unsubscribeNewHeads", + "chain_unsubscribeRuntimeVersion", + "childstate_getKeys", + "childstate_getKeysPaged", + "childstate_getKeysPagedAt", + "childstate_getStorage", + "childstate_getStorageEntries", + "childstate_getStorageHash", + "childstate_getStorageSize", + "debug_getBadBlocks", + "debug_getRawBlock", + "debug_getRawHeader", + "debug_getRawReceipts", + "debug_getRawTransaction", + "delegateInfo_getDelegate", + "delegateInfo_getDelegated", + "delegateInfo_getDelegates", + "eth_accounts", + "eth_blockNumber", + "eth_call", + "eth_chainId", + "eth_coinbase", + "eth_estimateGas", + "eth_feeHistory", + "eth_gasPrice", + "eth_getBalance", + "eth_getBlockByHash", + "eth_getBlockByNumber", + "eth_getBlockReceipts", + "eth_getBlockTransactionCountByHash", + "eth_getBlockTransactionCountByNumber", + "eth_getCode", + "eth_getFilterChanges", + "eth_getFilterLogs", + "eth_getLogs", + "eth_getStorageAt", + "eth_getTransactionByBlockHashAndIndex", + "eth_getTransactionByBlockNumberAndIndex", + "eth_getTransactionByHash", + "eth_getTransactionCount", + "eth_getTransactionReceipt", + "eth_getUncleByBlockHashAndIndex", + "eth_getUncleByBlockNumberAndIndex", + "eth_getUncleCountByBlockHash", + "eth_getUncleCountByBlockNumber", + "eth_getWork", + "eth_hashrate", + "eth_maxPriorityFeePerGas", + "eth_mining", + "eth_newBlockFilter", + "eth_newFilter", + "eth_newPendingTransactionFilter", + "eth_protocolVersion", + "eth_sendRawTransaction", + "eth_sendTransaction", + "eth_submitHashrate", + "eth_submitWork", + "eth_subscribe", + "eth_syncing", + "eth_uninstallFilter", + "eth_unsubscribe", + "net_listening", + "net_peerCount", + "net_version", + "neuronInfo_getNeuron", + "neuronInfo_getNeuronLite", + "neuronInfo_getNeurons", + "neuronInfo_getNeuronsLite", + "offchain_localStorageGet", + "offchain_localStorageSet", + "payment_queryFeeDetails", + "payment_queryInfo", + "rpc_methods", + "state_call", + "state_callAt", + "state_getChildReadProof", + "state_getKeys", + "state_getKeysPaged", + "state_getKeysPagedAt", + "state_getMetadata", + "state_getPairs", + "state_getReadProof", + "state_getRuntimeVersion", + "state_getStorage", + "state_getStorageAt", + "state_getStorageHash", + "state_getStorageHashAt", + "state_getStorageSize", + "state_getStorageSizeAt", + "state_queryStorage", + "state_queryStorageAt", + "state_subscribeRuntimeVersion", + "state_subscribeStorage", + "state_traceBlock", + "state_unsubscribeRuntimeVersion", + "state_unsubscribeStorage", + "subnetInfo_getLockCost", + "subnetInfo_getSubnetHyperparams", + "subnetInfo_getSubnetInfo", + "subnetInfo_getSubnetInfo_v2", + "subnetInfo_getSubnetsInf_v2", + "subnetInfo_getSubnetsInfo", + "subscribe_newHead", + "system_accountNextIndex", + "system_addLogFilter", + "system_addReservedPeer", + "system_chain", + "system_chainType", + "system_dryRun", + "system_dryRunAt", + "system_health", + "system_localListenAddresses", + "system_localPeerId", + "system_name", + "system_nodeRoles", + "system_peers", + "system_properties", + "system_removeReservedPeer", + "system_reservedPeers", + "system_resetLogFilter", + "system_syncState", + "system_unstable_networkState", + "system_version", + "transactionWatch_v1_submitAndWatch", + "transactionWatch_v1_unwatch", + "transaction_v1_broadcast", + "transaction_v1_stop", + "unsubscribe_newHead", + "web3_clientVersion", + "web3_sha3", + ] + }, + } + }, + "state_getRuntimeVersion": { + '["0x119e135f8ae6eac1f3fc47fc14838afcfdbee13e8dba14657ae150f4d56f6df2"]': { + "jsonrpc": "2.0", + "result": { + "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", 5], + ["0xe65b00e46cedd0aa", 2], + ["0x42e62be4a39e5b60", 1], + ["0x806df4ccaa9ed485", 1], + ["0x8375104b299b74c5", 1], + ["0x5d1fbfbe852f2807", 1], + ["0xc6886e2f8e598b0a", 1], + ], + "authoringVersion": 1, + "implName": "node-subtensor", + "implVersion": 1, + "specName": "node-subtensor", + "specVersion": 208, + "stateVersion": 1, + "transactionVersion": 1, + }, + } + }, + "state_getStorageAt": { + '["0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a430100", null]': { + "jsonrpc": "2.0", + "result": "0x01", + }, + '["0x658faa385070e074c85bf6b568cf0555696e262a16e52255a69d8acd793541460100", null]': { + "jsonrpc": "2.0", + "result": "0x080000000000000000cd46000000000000", + }, + }, + "system_chain": {"[]": {"jsonrpc": "2.0", "result": "Bittensor"}}, + }, + "bonds": { + "chain_getHead": { + "[]": { + "jsonrpc": "2.0", + "result": "0xba206b08f55118c21e6e243702052edcecd48e89d951fa05213adde358473da0", + } + }, + "chain_getHeader": { + '["0xba206b08f55118c21e6e243702052edcecd48e89d951fa05213adde358473da0"]': { + "jsonrpc": "2.0", + "result": { + "digest": { + "logs": [ + "0x066175726120a7489a0800000000", + "0x0466726f6e8801ca02a4c79abd1cb85d853af523456b599c5be96513b5b0fbba238258936b153900", + "0x0561757261010162d65ca730f2e564406a55e886ae6b82f7e54e9abdfd521e000040492c2f7f32420ab380b4b621a8cff2a8a35560b2b5d273eeb3f78069fd0098f5c872b8a981", + ] + }, + "extrinsicsRoot": "0xba5291f69aa51180b9642565836479a1483e1b5c12c9a9d2ad721b22da7df6d2", + "number": "0x31ce9a", + "parentHash": "0xf305f795b37cb91dbe8369cf67c0badc41dae5c15811ecfdad5fc101f3cc17f5", + "stateRoot": "0x85e42d411f708a50b10adf8e0e801834c82109edba89ee082b15f81317856d97", + }, + } + }, + "rpc_methods": { + "[]": { + "jsonrpc": "2.0", + "result": { + "methods": [ + "account_nextIndex", + "archive_unstable_body", + "archive_unstable_call", + "archive_unstable_finalizedHeight", + "archive_unstable_genesisHash", + "archive_unstable_hashByHeight", + "archive_unstable_header", + "archive_unstable_storage", + "author_hasKey", + "author_hasSessionKeys", + "author_insertKey", + "author_pendingExtrinsics", + "author_removeExtrinsic", + "author_rotateKeys", + "author_submitAndWatchExtrinsic", + "author_submitExtrinsic", + "author_unwatchExtrinsic", + "chainHead_v1_body", + "chainHead_v1_call", + "chainHead_v1_continue", + "chainHead_v1_follow", + "chainHead_v1_header", + "chainHead_v1_stopOperation", + "chainHead_v1_storage", + "chainHead_v1_unfollow", + "chainHead_v1_unpin", + "chainSpec_v1_chainName", + "chainSpec_v1_genesisHash", + "chainSpec_v1_properties", + "chain_getBlock", + "chain_getBlockHash", + "chain_getFinalisedHead", + "chain_getFinalizedHead", + "chain_getHead", + "chain_getHeader", + "chain_getRuntimeVersion", + "chain_subscribeAllHeads", + "chain_subscribeFinalisedHeads", + "chain_subscribeFinalizedHeads", + "chain_subscribeNewHead", + "chain_subscribeNewHeads", + "chain_subscribeRuntimeVersion", + "chain_unsubscribeAllHeads", + "chain_unsubscribeFinalisedHeads", + "chain_unsubscribeFinalizedHeads", + "chain_unsubscribeNewHead", + "chain_unsubscribeNewHeads", + "chain_unsubscribeRuntimeVersion", + "childstate_getKeys", + "childstate_getKeysPaged", + "childstate_getKeysPagedAt", + "childstate_getStorage", + "childstate_getStorageEntries", + "childstate_getStorageHash", + "childstate_getStorageSize", + "debug_getBadBlocks", + "debug_getRawBlock", + "debug_getRawHeader", + "debug_getRawReceipts", + "debug_getRawTransaction", + "delegateInfo_getDelegate", + "delegateInfo_getDelegated", + "delegateInfo_getDelegates", + "eth_accounts", + "eth_blockNumber", + "eth_call", + "eth_chainId", + "eth_coinbase", + "eth_estimateGas", + "eth_feeHistory", + "eth_gasPrice", + "eth_getBalance", + "eth_getBlockByHash", + "eth_getBlockByNumber", + "eth_getBlockReceipts", + "eth_getBlockTransactionCountByHash", + "eth_getBlockTransactionCountByNumber", + "eth_getCode", + "eth_getFilterChanges", + "eth_getFilterLogs", + "eth_getLogs", + "eth_getStorageAt", + "eth_getTransactionByBlockHashAndIndex", + "eth_getTransactionByBlockNumberAndIndex", + "eth_getTransactionByHash", + "eth_getTransactionCount", + "eth_getTransactionReceipt", + "eth_getUncleByBlockHashAndIndex", + "eth_getUncleByBlockNumberAndIndex", + "eth_getUncleCountByBlockHash", + "eth_getUncleCountByBlockNumber", + "eth_getWork", + "eth_hashrate", + "eth_maxPriorityFeePerGas", + "eth_mining", + "eth_newBlockFilter", + "eth_newFilter", + "eth_newPendingTransactionFilter", + "eth_protocolVersion", + "eth_sendRawTransaction", + "eth_sendTransaction", + "eth_submitHashrate", + "eth_submitWork", + "eth_subscribe", + "eth_syncing", + "eth_uninstallFilter", + "eth_unsubscribe", + "net_listening", + "net_peerCount", + "net_version", + "neuronInfo_getNeuron", + "neuronInfo_getNeuronLite", + "neuronInfo_getNeurons", + "neuronInfo_getNeuronsLite", + "offchain_localStorageGet", + "offchain_localStorageSet", + "payment_queryFeeDetails", + "payment_queryInfo", + "rpc_methods", + "state_call", + "state_callAt", + "state_getChildReadProof", + "state_getKeys", + "state_getKeysPaged", + "state_getKeysPagedAt", + "state_getMetadata", + "state_getPairs", + "state_getReadProof", + "state_getRuntimeVersion", + "state_getStorage", + "state_getStorageAt", + "state_getStorageHash", + "state_getStorageHashAt", + "state_getStorageSize", + "state_getStorageSizeAt", + "state_queryStorage", + "state_queryStorageAt", + "state_subscribeRuntimeVersion", + "state_subscribeStorage", + "state_traceBlock", + "state_unsubscribeRuntimeVersion", + "state_unsubscribeStorage", + "subnetInfo_getLockCost", + "subnetInfo_getSubnetHyperparams", + "subnetInfo_getSubnetInfo", + "subnetInfo_getSubnetInfo_v2", + "subnetInfo_getSubnetsInf_v2", + "subnetInfo_getSubnetsInfo", + "subscribe_newHead", + "system_accountNextIndex", + "system_addLogFilter", + "system_addReservedPeer", + "system_chain", + "system_chainType", + "system_dryRun", + "system_dryRunAt", + "system_health", + "system_localListenAddresses", + "system_localPeerId", + "system_name", + "system_nodeRoles", + "system_peers", + "system_properties", + "system_removeReservedPeer", + "system_reservedPeers", + "system_resetLogFilter", + "system_syncState", + "system_unstable_networkState", + "system_version", + "transactionWatch_v1_submitAndWatch", + "transactionWatch_v1_unwatch", + "transaction_v1_broadcast", + "transaction_v1_stop", + "unsubscribe_newHead", + "web3_clientVersion", + "web3_sha3", + ] + }, + } + }, + "state_getKeysPaged": { + '["0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab01700", 100, "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab01700", "0xba206b08f55118c21e6e243702052edcecd48e89d951fa05213adde358473da0"]': { + "jsonrpc": "2.0", + "result": [ + "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000000", + "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000100", + "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000200", + "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000300", + "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000400", + "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000500", + "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000600", + "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000700", + "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000800", + "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000900", + "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000a00", + "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000b00", + "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000c00", + "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000d00", + "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000e00", + "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000f00", + "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017001000", + "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017001100", + "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017001200", + ], + }, + '["0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab01700", 100, "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017001200", "0xba206b08f55118c21e6e243702052edcecd48e89d951fa05213adde358473da0"]': { + "jsonrpc": "2.0", + "result": [], + }, + }, + "state_getRuntimeVersion": { + '["0xf305f795b37cb91dbe8369cf67c0badc41dae5c15811ecfdad5fc101f3cc17f5"]': { + "jsonrpc": "2.0", + "result": { + "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", 5], + ["0xe65b00e46cedd0aa", 2], + ["0x42e62be4a39e5b60", 1], + ["0x806df4ccaa9ed485", 1], + ["0x8375104b299b74c5", 1], + ["0x5d1fbfbe852f2807", 1], + ["0xc6886e2f8e598b0a", 1], + ], + "authoringVersion": 1, + "implName": "node-subtensor", + "implVersion": 1, + "specName": "node-subtensor", + "specVersion": 208, + "stateVersion": 1, + "transactionVersion": 1, + }, + } + }, + "state_queryStorageAt": { + '[["0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000000", "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000100", "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000200", "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000300", "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000400", "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000500", "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000600", "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000700", "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000800", "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000900", "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000a00", "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000b00", "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000c00", "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000d00", "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000e00", "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000f00", "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017001000", "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017001100", "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017001200"], "0xba206b08f55118c21e6e243702052edcecd48e89d951fa05213adde358473da0"]': { + "jsonrpc": "2.0", + "result": [ + { + "block": "0xba206b08f55118c21e6e243702052edcecd48e89d951fa05213adde358473da0", + "changes": [ + [ + "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000000", + "0x00", + ], + [ + "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000100", + "0x00", + ], + [ + "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000200", + "0x00", + ], + [ + "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000300", + "0x00", + ], + [ + "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000400", + "0x00", + ], + [ + "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000500", + "0x00", + ], + [ + "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000600", + "0x00", + ], + [ + "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000700", + "0x00", + ], + [ + "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000800", + "0x00", + ], + [ + "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000900", + "0x00", + ], + [ + "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000a00", + "0x00", + ], + [ + "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000b00", + "0x00", + ], + [ + "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000c00", + "0x00", + ], + [ + "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000d00", + "0x00", + ], + [ + "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000e00", + "0x00", + ], + [ + "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000f00", + "0x00", + ], + [ + "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017001000", + "0x00", + ], + [ + "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017001100", + "0x00", + ], + [ + "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017001200", + "0x00", + ], + ], + } + ], + } + }, + "system_chain": {"[]": {"jsonrpc": "2.0", "result": "Bittensor"}}, + }, + "difficulty": { + "chain_getHead": { + "[]": { + "jsonrpc": "2.0", + "result": "0xef1f94e2a46fbd2b39f60d855da491ee6e2637d9c6092358f8ce97b6839973ae", + } + }, + "chain_getHeader": { + '["0xef1f94e2a46fbd2b39f60d855da491ee6e2637d9c6092358f8ce97b6839973ae"]': { + "jsonrpc": "2.0", + "result": { + "digest": { + "logs": [ + "0x066175726120ae489a0800000000", + "0x0466726f6e8801e6315ae1138f6d998b7a67af7473bf3f811791a5427fd0335d33b140f79e811800", + "0x05617572610101c2b3d49809ccc446a0237b04b8429b8a92bf9555506897b406c605008efc28755adb7f5a261017006a386f401497e6edda9e417aca0cf2d6bd81d6dd2b86c08d", + ] + }, + "extrinsicsRoot": "0xdefbb162adbbecaa3a3cad493c55ee69556b194ece6893f711d689704743580f", + "number": "0x31cea1", + "parentHash": "0xa0ee9e0d47444a8570eec6e4e48120bba7aef6fcce277d90cce6d48655398459", + "stateRoot": "0x84c348ee245bf2ce1869223dfcadb5d2a37331d5f00dcef879402de2a0f4c0bd", + }, + } + }, + "rpc_methods": { + "[]": { + "jsonrpc": "2.0", + "result": { + "methods": [ + "account_nextIndex", + "archive_unstable_body", + "archive_unstable_call", + "archive_unstable_finalizedHeight", + "archive_unstable_genesisHash", + "archive_unstable_hashByHeight", + "archive_unstable_header", + "archive_unstable_storage", + "author_hasKey", + "author_hasSessionKeys", + "author_insertKey", + "author_pendingExtrinsics", + "author_removeExtrinsic", + "author_rotateKeys", + "author_submitAndWatchExtrinsic", + "author_submitExtrinsic", + "author_unwatchExtrinsic", + "chainHead_v1_body", + "chainHead_v1_call", + "chainHead_v1_continue", + "chainHead_v1_follow", + "chainHead_v1_header", + "chainHead_v1_stopOperation", + "chainHead_v1_storage", + "chainHead_v1_unfollow", + "chainHead_v1_unpin", + "chainSpec_v1_chainName", + "chainSpec_v1_genesisHash", + "chainSpec_v1_properties", + "chain_getBlock", + "chain_getBlockHash", + "chain_getFinalisedHead", + "chain_getFinalizedHead", + "chain_getHead", + "chain_getHeader", + "chain_getRuntimeVersion", + "chain_subscribeAllHeads", + "chain_subscribeFinalisedHeads", + "chain_subscribeFinalizedHeads", + "chain_subscribeNewHead", + "chain_subscribeNewHeads", + "chain_subscribeRuntimeVersion", + "chain_unsubscribeAllHeads", + "chain_unsubscribeFinalisedHeads", + "chain_unsubscribeFinalizedHeads", + "chain_unsubscribeNewHead", + "chain_unsubscribeNewHeads", + "chain_unsubscribeRuntimeVersion", + "childstate_getKeys", + "childstate_getKeysPaged", + "childstate_getKeysPagedAt", + "childstate_getStorage", + "childstate_getStorageEntries", + "childstate_getStorageHash", + "childstate_getStorageSize", + "debug_getBadBlocks", + "debug_getRawBlock", + "debug_getRawHeader", + "debug_getRawReceipts", + "debug_getRawTransaction", + "delegateInfo_getDelegate", + "delegateInfo_getDelegated", + "delegateInfo_getDelegates", + "eth_accounts", + "eth_blockNumber", + "eth_call", + "eth_chainId", + "eth_coinbase", + "eth_estimateGas", + "eth_feeHistory", + "eth_gasPrice", + "eth_getBalance", + "eth_getBlockByHash", + "eth_getBlockByNumber", + "eth_getBlockReceipts", + "eth_getBlockTransactionCountByHash", + "eth_getBlockTransactionCountByNumber", + "eth_getCode", + "eth_getFilterChanges", + "eth_getFilterLogs", + "eth_getLogs", + "eth_getStorageAt", + "eth_getTransactionByBlockHashAndIndex", + "eth_getTransactionByBlockNumberAndIndex", + "eth_getTransactionByHash", + "eth_getTransactionCount", + "eth_getTransactionReceipt", + "eth_getUncleByBlockHashAndIndex", + "eth_getUncleByBlockNumberAndIndex", + "eth_getUncleCountByBlockHash", + "eth_getUncleCountByBlockNumber", + "eth_getWork", + "eth_hashrate", + "eth_maxPriorityFeePerGas", + "eth_mining", + "eth_newBlockFilter", + "eth_newFilter", + "eth_newPendingTransactionFilter", + "eth_protocolVersion", + "eth_sendRawTransaction", + "eth_sendTransaction", + "eth_submitHashrate", + "eth_submitWork", + "eth_subscribe", + "eth_syncing", + "eth_uninstallFilter", + "eth_unsubscribe", + "net_listening", + "net_peerCount", + "net_version", + "neuronInfo_getNeuron", + "neuronInfo_getNeuronLite", + "neuronInfo_getNeurons", + "neuronInfo_getNeuronsLite", + "offchain_localStorageGet", + "offchain_localStorageSet", + "payment_queryFeeDetails", + "payment_queryInfo", + "rpc_methods", + "state_call", + "state_callAt", + "state_getChildReadProof", + "state_getKeys", + "state_getKeysPaged", + "state_getKeysPagedAt", + "state_getMetadata", + "state_getPairs", + "state_getReadProof", + "state_getRuntimeVersion", + "state_getStorage", + "state_getStorageAt", + "state_getStorageHash", + "state_getStorageHashAt", + "state_getStorageSize", + "state_getStorageSizeAt", + "state_queryStorage", + "state_queryStorageAt", + "state_subscribeRuntimeVersion", + "state_subscribeStorage", + "state_traceBlock", + "state_unsubscribeRuntimeVersion", + "state_unsubscribeStorage", + "subnetInfo_getLockCost", + "subnetInfo_getSubnetHyperparams", + "subnetInfo_getSubnetInfo", + "subnetInfo_getSubnetInfo_v2", + "subnetInfo_getSubnetsInf_v2", + "subnetInfo_getSubnetsInfo", + "subscribe_newHead", + "system_accountNextIndex", + "system_addLogFilter", + "system_addReservedPeer", + "system_chain", + "system_chainType", + "system_dryRun", + "system_dryRunAt", + "system_health", + "system_localListenAddresses", + "system_localPeerId", + "system_name", + "system_nodeRoles", + "system_peers", + "system_properties", + "system_removeReservedPeer", + "system_reservedPeers", + "system_resetLogFilter", + "system_syncState", + "system_unstable_networkState", + "system_version", + "transactionWatch_v1_submitAndWatch", + "transactionWatch_v1_unwatch", + "transaction_v1_broadcast", + "transaction_v1_stop", + "unsubscribe_newHead", + "web3_clientVersion", + "web3_sha3", + ] + }, + } + }, + "state_getRuntimeVersion": { + '["0xa0ee9e0d47444a8570eec6e4e48120bba7aef6fcce277d90cce6d48655398459"]': { + "jsonrpc": "2.0", + "result": { + "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", 5], + ["0xe65b00e46cedd0aa", 2], + ["0x42e62be4a39e5b60", 1], + ["0x806df4ccaa9ed485", 1], + ["0x8375104b299b74c5", 1], + ["0x5d1fbfbe852f2807", 1], + ["0xc6886e2f8e598b0a", 1], + ], + "authoringVersion": 1, + "implName": "node-subtensor", + "implVersion": 1, + "specName": "node-subtensor", + "specVersion": 208, + "stateVersion": 1, + "transactionVersion": 1, + }, + } + }, + "state_getStorageAt": { + '["0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a431700", "0xef1f94e2a46fbd2b39f60d855da491ee6e2637d9c6092358f8ce97b6839973ae"]': { + "jsonrpc": "2.0", + "result": "0x01", + }, + '["0x658faa385070e074c85bf6b568cf05557d15dd66fbf0cbda1d3a651b5e606df21700", "0xef1f94e2a46fbd2b39f60d855da491ee6e2637d9c6092358f8ce97b6839973ae"]': { + "jsonrpc": "2.0", + "result": "0x8096980000000000", + }, + }, + "system_chain": {"[]": {"jsonrpc": "2.0", "result": "Bittensor"}}, + }, + "get_all_subnets_info": { + "chain_getHead": { + "[]": { + "jsonrpc": "2.0", + "result": "0x1b3f85dc8c83cada6d1782c955973a509a37c62af3d6ba29f5fd816aee37eb3b", + } + }, + "chain_getHeader": { + '["0x1b3f85dc8c83cada6d1782c955973a509a37c62af3d6ba29f5fd816aee37eb3b"]': { + "jsonrpc": "2.0", + "result": { + "digest": { + "logs": [ + "0x066175726120ab489a0800000000", + "0x0466726f6e88015b559367fb27665be180a32109a44b153dd14cafb22eb908ac9e79a308916c9a00", + "0x056175726101012030b4a878800704102801d12f2dfe4b3f66bd8c3fbe236e4881ed1b9bdbaf6c8eb6200f9db52945187c3c1f46d8b4c08c1b50e2002d94594e0c751069ca7d8d", + ] + }, + "extrinsicsRoot": "0xf7eaa2bb07e7650b27ee819ee07c987207ad40220f250e4eebb1178e19734242", + "number": "0x31ce9e", + "parentHash": "0xd867f39bb9e2a626945183af01065b61d0695d435a765bfd0ce5f821fbed8f82", + "stateRoot": "0x3096ed3b5859801b59e2fb348ef560e87f473a799dea9be12982b969fdd1dfaf", + }, + } + }, + "state_call": { + '["SubnetInfoRuntimeApi_get_subnets_info_v2", "", null]': { + "jsonrpc": "2.0", + "result": "0x08010028feff0100025a62020140010100feff0300c80001015d01910100000002286bee000000000000000000000000000000000000000000000000000000000000000000010428feff0100025a62020140010100feff0300c804010461019101000002286bee02286bee000000000000000000000000000000000000000000000000000000000000000000", + }, + }, + "state_getRuntimeVersion": { + '["0xd867f39bb9e2a626945183af01065b61d0695d435a765bfd0ce5f821fbed8f82"]': { + "jsonrpc": "2.0", + "result": { + "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", 5], + ["0xe65b00e46cedd0aa", 2], + ["0x42e62be4a39e5b60", 1], + ["0x806df4ccaa9ed485", 1], + ["0x8375104b299b74c5", 1], + ["0x5d1fbfbe852f2807", 1], + ["0xc6886e2f8e598b0a", 1], + ], + "authoringVersion": 1, + "implName": "node-subtensor", + "implVersion": 1, + "specName": "node-subtensor", + "specVersion": 208, + "stateVersion": 1, + "transactionVersion": 1, + }, + } + }, + "system_chain": {"[]": {"jsonrpc": "2.0", "result": "Bittensor"}}, + }, + "get_balance": { + "chain_getHead": { + "[]": { + "jsonrpc": "2.0", + "result": "0x1b3f85dc8c83cada6d1782c955973a509a37c62af3d6ba29f5fd816aee37eb3b", + } + }, + "chain_getHeader": { + '["0x1b3f85dc8c83cada6d1782c955973a509a37c62af3d6ba29f5fd816aee37eb3b"]': { + "jsonrpc": "2.0", + "result": { + "digest": { + "logs": [ + "0x066175726120ab489a0800000000", + "0x0466726f6e88015b559367fb27665be180a32109a44b153dd14cafb22eb908ac9e79a308916c9a00", + "0x056175726101012030b4a878800704102801d12f2dfe4b3f66bd8c3fbe236e4881ed1b9bdbaf6c8eb6200f9db52945187c3c1f46d8b4c08c1b50e2002d94594e0c751069ca7d8d", + ] + }, + "extrinsicsRoot": "0xf7eaa2bb07e7650b27ee819ee07c987207ad40220f250e4eebb1178e19734242", + "number": "0x31ce9e", + "parentHash": "0xd867f39bb9e2a626945183af01065b61d0695d435a765bfd0ce5f821fbed8f82", + "stateRoot": "0x3096ed3b5859801b59e2fb348ef560e87f473a799dea9be12982b969fdd1dfaf", + }, + } + }, + "rpc_methods": { + "[]": { + "jsonrpc": "2.0", + "result": { + "methods": [ + "account_nextIndex", + "archive_unstable_body", + "archive_unstable_call", + "archive_unstable_finalizedHeight", + "archive_unstable_genesisHash", + "archive_unstable_hashByHeight", + "archive_unstable_header", + "archive_unstable_storage", + "author_hasKey", + "author_hasSessionKeys", + "author_insertKey", + "author_pendingExtrinsics", + "author_removeExtrinsic", + "author_rotateKeys", + "author_submitAndWatchExtrinsic", + "author_submitExtrinsic", + "author_unwatchExtrinsic", + "chainHead_v1_body", + "chainHead_v1_call", + "chainHead_v1_continue", + "chainHead_v1_follow", + "chainHead_v1_header", + "chainHead_v1_stopOperation", + "chainHead_v1_storage", + "chainHead_v1_unfollow", + "chainHead_v1_unpin", + "chainSpec_v1_chainName", + "chainSpec_v1_genesisHash", + "chainSpec_v1_properties", + "chain_getBlock", + "chain_getBlockHash", + "chain_getFinalisedHead", + "chain_getFinalizedHead", + "chain_getHead", + "chain_getHeader", + "chain_getRuntimeVersion", + "chain_subscribeAllHeads", + "chain_subscribeFinalisedHeads", + "chain_subscribeFinalizedHeads", + "chain_subscribeNewHead", + "chain_subscribeNewHeads", + "chain_subscribeRuntimeVersion", + "chain_unsubscribeAllHeads", + "chain_unsubscribeFinalisedHeads", + "chain_unsubscribeFinalizedHeads", + "chain_unsubscribeNewHead", + "chain_unsubscribeNewHeads", + "chain_unsubscribeRuntimeVersion", + "childstate_getKeys", + "childstate_getKeysPaged", + "childstate_getKeysPagedAt", + "childstate_getStorage", + "childstate_getStorageEntries", + "childstate_getStorageHash", + "childstate_getStorageSize", + "debug_getBadBlocks", + "debug_getRawBlock", + "debug_getRawHeader", + "debug_getRawReceipts", + "debug_getRawTransaction", + "delegateInfo_getDelegate", + "delegateInfo_getDelegated", + "delegateInfo_getDelegates", + "eth_accounts", + "eth_blockNumber", + "eth_call", + "eth_chainId", + "eth_coinbase", + "eth_estimateGas", + "eth_feeHistory", + "eth_gasPrice", + "eth_getBalance", + "eth_getBlockByHash", + "eth_getBlockByNumber", + "eth_getBlockReceipts", + "eth_getBlockTransactionCountByHash", + "eth_getBlockTransactionCountByNumber", + "eth_getCode", + "eth_getFilterChanges", + "eth_getFilterLogs", + "eth_getLogs", + "eth_getStorageAt", + "eth_getTransactionByBlockHashAndIndex", + "eth_getTransactionByBlockNumberAndIndex", + "eth_getTransactionByHash", + "eth_getTransactionCount", + "eth_getTransactionReceipt", + "eth_getUncleByBlockHashAndIndex", + "eth_getUncleByBlockNumberAndIndex", + "eth_getUncleCountByBlockHash", + "eth_getUncleCountByBlockNumber", + "eth_getWork", + "eth_hashrate", + "eth_maxPriorityFeePerGas", + "eth_mining", + "eth_newBlockFilter", + "eth_newFilter", + "eth_newPendingTransactionFilter", + "eth_protocolVersion", + "eth_sendRawTransaction", + "eth_sendTransaction", + "eth_submitHashrate", + "eth_submitWork", + "eth_subscribe", + "eth_syncing", + "eth_uninstallFilter", + "eth_unsubscribe", + "net_listening", + "net_peerCount", + "net_version", + "neuronInfo_getNeuron", + "neuronInfo_getNeuronLite", + "neuronInfo_getNeurons", + "neuronInfo_getNeuronsLite", + "offchain_localStorageGet", + "offchain_localStorageSet", + "payment_queryFeeDetails", + "payment_queryInfo", + "rpc_methods", + "state_call", + "state_callAt", + "state_getChildReadProof", + "state_getKeys", + "state_getKeysPaged", + "state_getKeysPagedAt", + "state_getMetadata", + "state_getPairs", + "state_getReadProof", + "state_getRuntimeVersion", + "state_getStorage", + "state_getStorageAt", + "state_getStorageHash", + "state_getStorageHashAt", + "state_getStorageSize", + "state_getStorageSizeAt", + "state_queryStorage", + "state_queryStorageAt", + "state_subscribeRuntimeVersion", + "state_subscribeStorage", + "state_traceBlock", + "state_unsubscribeRuntimeVersion", + "state_unsubscribeStorage", + "subnetInfo_getLockCost", + "subnetInfo_getSubnetHyperparams", + "subnetInfo_getSubnetInfo", + "subnetInfo_getSubnetInfo_v2", + "subnetInfo_getSubnetsInf_v2", + "subnetInfo_getSubnetsInfo", + "subscribe_newHead", + "system_accountNextIndex", + "system_addLogFilter", + "system_addReservedPeer", + "system_chain", + "system_chainType", + "system_dryRun", + "system_dryRunAt", + "system_health", + "system_localListenAddresses", + "system_localPeerId", + "system_name", + "system_nodeRoles", + "system_peers", + "system_properties", + "system_removeReservedPeer", + "system_reservedPeers", + "system_resetLogFilter", + "system_syncState", + "system_unstable_networkState", + "system_version", + "transactionWatch_v1_submitAndWatch", + "transactionWatch_v1_unwatch", + "transaction_v1_broadcast", + "transaction_v1_stop", + "unsubscribe_newHead", + "web3_clientVersion", + "web3_sha3", + ] + }, + } + }, + "state_getRuntimeVersion": { + '["0xd867f39bb9e2a626945183af01065b61d0695d435a765bfd0ce5f821fbed8f82"]': { + "jsonrpc": "2.0", + "result": { + "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", 5], + ["0xe65b00e46cedd0aa", 2], + ["0x42e62be4a39e5b60", 1], + ["0x806df4ccaa9ed485", 1], + ["0x8375104b299b74c5", 1], + ["0x5d1fbfbe852f2807", 1], + ["0xc6886e2f8e598b0a", 1], + ], + "authoringVersion": 1, + "implName": "node-subtensor", + "implVersion": 1, + "specName": "node-subtensor", + "specVersion": 208, + "stateVersion": 1, + "transactionVersion": 1, + }, + } + }, + "state_getStorageAt": { + '["0x26aa394eea5630e07c48ae0c9558cef7b99d880ec681799c0cf30e8886371da911b12ddffa80cb6f18baa803af02c1d71257de87a71abb164403f7cb9ea65d0721f8bb89d8bd85754deceedd91e86053", "0x1b3f85dc8c83cada6d1782c955973a509a37c62af3d6ba29f5fd816aee37eb3b"]': { + "jsonrpc": "2.0", + "result": "0x8d000000010000000100000000000000d0c30fbc0100000000e1f50500000000000000000000000000000000000000000000000000000080", + } + }, + "system_chain": {"[]": {"jsonrpc": "2.0", "result": "Bittensor"}}, + }, + "get_block_hash": { + "chain_getBlockHash": { + "[3234677]": { + "jsonrpc": "2.0", + "result": "0xe89482ae7892ab5633f294179245f4058a99781e15f21da31eb625169da5d409", + } + }, + "system_chain": {"[]": {"jsonrpc": "2.0", "result": "Bittensor"}}, + }, + "get_commitment": { + "chain_getHead": { + "[]": { + "jsonrpc": "2.0", + "result": "0xa049374c707f9a426866a090c66eff7106a75597dd0d87a8fbe39a7f7887ed16", + } + }, + "chain_getHeader": { + '["0xa049374c707f9a426866a090c66eff7106a75597dd0d87a8fbe39a7f7887ed16"]': { + "jsonrpc": "2.0", + "result": { + "digest": { + "logs": [ + "0x066175726120a4489a0800000000", + "0x0466726f6e88010f9ddbd627a7831fa708913774b9c4dc6bd6b5e67c7bc202212ec123eafe0fea00", + "0x056175726101015e937529bffb8837c11a4784fa1638a519eada7bb9a98c484917a6ce84e92f24cafbe4a2e057813f52488d37ad9bd0402f03dac6ed62bc8f0f678bd3326bdc85", + ] + }, + "extrinsicsRoot": "0x3d4fcc97011a4f12d9c64074b37a39a440c9fdfb38cd4f4923be189c36a55fc2", + "number": "0x31ce97", + "parentHash": "0x03a008636efbe7706f79f2c7d7d4e85ff26a3e67922e5fefab71e7f6d9dab43d", + "stateRoot": "0x0f0943f73c54721a2e4ef101e7de798b8b8e4e72681703d694dde6df3d797d82", + }, + }, + "[null]": { + "jsonrpc": "2.0", + "result": { + "digest": { + "logs": [ + "0x066175726120a4489a0800000000", + "0x0466726f6e88010f9ddbd627a7831fa708913774b9c4dc6bd6b5e67c7bc202212ec123eafe0fea00", + "0x056175726101015e937529bffb8837c11a4784fa1638a519eada7bb9a98c484917a6ce84e92f24cafbe4a2e057813f52488d37ad9bd0402f03dac6ed62bc8f0f678bd3326bdc85", + ] + }, + "extrinsicsRoot": "0x3d4fcc97011a4f12d9c64074b37a39a440c9fdfb38cd4f4923be189c36a55fc2", + "number": "0x31ce97", + "parentHash": "0x03a008636efbe7706f79f2c7d7d4e85ff26a3e67922e5fefab71e7f6d9dab43d", + "stateRoot": "0x0f0943f73c54721a2e4ef101e7de798b8b8e4e72681703d694dde6df3d797d82", + }, + }, + }, + "rpc_methods": { + "[]": { + "jsonrpc": "2.0", + "result": { + "methods": [ + "account_nextIndex", + "archive_unstable_body", + "archive_unstable_call", + "archive_unstable_finalizedHeight", + "archive_unstable_genesisHash", + "archive_unstable_hashByHeight", + "archive_unstable_header", + "archive_unstable_storage", + "author_hasKey", + "author_hasSessionKeys", + "author_insertKey", + "author_pendingExtrinsics", + "author_removeExtrinsic", + "author_rotateKeys", + "author_submitAndWatchExtrinsic", + "author_submitExtrinsic", + "author_unwatchExtrinsic", + "chainHead_v1_body", + "chainHead_v1_call", + "chainHead_v1_continue", + "chainHead_v1_follow", + "chainHead_v1_header", + "chainHead_v1_stopOperation", + "chainHead_v1_storage", + "chainHead_v1_unfollow", + "chainHead_v1_unpin", + "chainSpec_v1_chainName", + "chainSpec_v1_genesisHash", + "chainSpec_v1_properties", + "chain_getBlock", + "chain_getBlockHash", + "chain_getFinalisedHead", + "chain_getFinalizedHead", + "chain_getHead", + "chain_getHeader", + "chain_getRuntimeVersion", + "chain_subscribeAllHeads", + "chain_subscribeFinalisedHeads", + "chain_subscribeFinalizedHeads", + "chain_subscribeNewHead", + "chain_subscribeNewHeads", + "chain_subscribeRuntimeVersion", + "chain_unsubscribeAllHeads", + "chain_unsubscribeFinalisedHeads", + "chain_unsubscribeFinalizedHeads", + "chain_unsubscribeNewHead", + "chain_unsubscribeNewHeads", + "chain_unsubscribeRuntimeVersion", + "childstate_getKeys", + "childstate_getKeysPaged", + "childstate_getKeysPagedAt", + "childstate_getStorage", + "childstate_getStorageEntries", + "childstate_getStorageHash", + "childstate_getStorageSize", + "debug_getBadBlocks", + "debug_getRawBlock", + "debug_getRawHeader", + "debug_getRawReceipts", + "debug_getRawTransaction", + "delegateInfo_getDelegate", + "delegateInfo_getDelegated", + "delegateInfo_getDelegates", + "eth_accounts", + "eth_blockNumber", + "eth_call", + "eth_chainId", + "eth_coinbase", + "eth_estimateGas", + "eth_feeHistory", + "eth_gasPrice", + "eth_getBalance", + "eth_getBlockByHash", + "eth_getBlockByNumber", + "eth_getBlockReceipts", + "eth_getBlockTransactionCountByHash", + "eth_getBlockTransactionCountByNumber", + "eth_getCode", + "eth_getFilterChanges", + "eth_getFilterLogs", + "eth_getLogs", + "eth_getStorageAt", + "eth_getTransactionByBlockHashAndIndex", + "eth_getTransactionByBlockNumberAndIndex", + "eth_getTransactionByHash", + "eth_getTransactionCount", + "eth_getTransactionReceipt", + "eth_getUncleByBlockHashAndIndex", + "eth_getUncleByBlockNumberAndIndex", + "eth_getUncleCountByBlockHash", + "eth_getUncleCountByBlockNumber", + "eth_getWork", + "eth_hashrate", + "eth_maxPriorityFeePerGas", + "eth_mining", + "eth_newBlockFilter", + "eth_newFilter", + "eth_newPendingTransactionFilter", + "eth_protocolVersion", + "eth_sendRawTransaction", + "eth_sendTransaction", + "eth_submitHashrate", + "eth_submitWork", + "eth_subscribe", + "eth_syncing", + "eth_uninstallFilter", + "eth_unsubscribe", + "net_listening", + "net_peerCount", + "net_version", + "neuronInfo_getNeuron", + "neuronInfo_getNeuronLite", + "neuronInfo_getNeurons", + "neuronInfo_getNeuronsLite", + "offchain_localStorageGet", + "offchain_localStorageSet", + "payment_queryFeeDetails", + "payment_queryInfo", + "rpc_methods", + "state_call", + "state_callAt", + "state_getChildReadProof", + "state_getKeys", + "state_getKeysPaged", + "state_getKeysPagedAt", + "state_getMetadata", + "state_getPairs", + "state_getReadProof", + "state_getRuntimeVersion", + "state_getStorage", + "state_getStorageAt", + "state_getStorageHash", + "state_getStorageHashAt", + "state_getStorageSize", + "state_getStorageSizeAt", + "state_queryStorage", + "state_queryStorageAt", + "state_subscribeRuntimeVersion", + "state_subscribeStorage", + "state_traceBlock", + "state_unsubscribeRuntimeVersion", + "state_unsubscribeStorage", + "subnetInfo_getLockCost", + "subnetInfo_getSubnetHyperparams", + "subnetInfo_getSubnetInfo", + "subnetInfo_getSubnetInfo_v2", + "subnetInfo_getSubnetsInf_v2", + "subnetInfo_getSubnetsInfo", + "subscribe_newHead", + "system_accountNextIndex", + "system_addLogFilter", + "system_addReservedPeer", + "system_chain", + "system_chainType", + "system_dryRun", + "system_dryRunAt", + "system_health", + "system_localListenAddresses", + "system_localPeerId", + "system_name", + "system_nodeRoles", + "system_peers", + "system_properties", + "system_removeReservedPeer", + "system_reservedPeers", + "system_resetLogFilter", + "system_syncState", + "system_unstable_networkState", + "system_version", + "transactionWatch_v1_submitAndWatch", + "transactionWatch_v1_unwatch", + "transaction_v1_broadcast", + "transaction_v1_stop", + "unsubscribe_newHead", + "web3_clientVersion", + "web3_sha3", + ] + }, + } + }, + "state_call": { + '["NeuronInfoRuntimeApi_get_neurons_lite", "0x1700"]': { + "jsonrpc": "2.0", + "result": "0x35364cb26667d9765b6aaa76c1b7f0f2786c03257a090081bb483b1e6862e409f64e3f6aa1c0384f95f3ec8f764309523a653a5bd814632fe2eff931d35c606a5e0c65005c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000046aa1c0384f95f3ec8f764309523a653a5bd814632fe2eff931d35c606a5e0c65620bb70000000000000000825b53000100c0dcbc2b619e07312e6fa2e1b9c91eb6826c77ab732fa1670bfa175c3cc06b01d89c740d9dfede764de5284c89dcedac74aa23b2d303a668e251620b78d79949045c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004d89c740d9dfede764de5284c89dcedac74aa23b2d303a668e251620b78d7994907f1a79e0402000000000000003aac57000100e6e23c10ab6ed114577d8fa56f976d350aabea07e0382c6ab0313f1fd5b4ed7c9aa545ce482f48c3160fa706bbafe923bb8248403141729d95c96699b615f04d085c00e390160000000000b40200001329d44300000000000000000000000049080404000000000000000000000000000000000000000000000000000000000000000000049aa545ce482f48c3160fa706bbafe923bb8248403141729d95c96699b615f04d000000000000000022435a000100866eda07c029ea2856de08c183ddff461cb6ce83f8ec49150ef5647eee8f4c6b40469c545b3d0396a76ca1c41a6c090266e2e532adc9fde080d9e90d2e7b093d0c5c00dfb8190000000000b5020000026fb5d50000000000000000000000009aac04040000000000000000000000000000000000000000000000000000000000000000000440469c545b3d0396a76ca1c41a6c090266e2e532adc9fde080d9e90d2e7b093dea2b0e000000000000000046d9660001000cc6dd507099d72736a92d6f889e4024e3431ac57eb9eb1fe0ca3302c6d4c867509b91e2cf336358ec2f3fe6e0598e2909c29a57597765240dfa8db41e7dd46a105c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004509b91e2cf336358ec2f3fe6e0598e2909c29a57597765240dfa8db41e7dd46a00000000000000005a9f6f0001004af7598952f87683204a2a0c2266f941a74899bd69b0ce56e42a82820f6894461257de87a71abb164403f7cb9ea65d0721f8bb89d8bd85754deceedd91e86053145c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000041257de87a71abb164403f7cb9ea65d0721f8bb89d8bd85754deceedd91e8605303b0ec68b3000000000000006e4478000100ba7c215ab878d9231d11bdaa5e4717b90d3b85df986abc669de607b1da6a020c74d879f5f872947565597d2f0c310fa0658d168ecd68a2ad3d5ec63438561643185c00265e1e0000000000b5020000583937a2000000000000000000000000d54f04040000000000000000000000000000000000000000000000000000000000000000000474d879f5f872947565597d2f0c310fa0658d168ecd68a2ad3d5ec6343856164307fd5a63c402000000000000004a7579000100084667e9ab96aca6c12c3094d740142cc4d36e423ede27b752978ffc70841b4dda618f84f833dc38b54f31cca63c408134bcce60f38c16c2f64fd4cf37068b0f1c5c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004da618f84f833dc38b54f31cca63c408134bcce60f38c16c2f64fd4cf37068b0f07bfb78a48170000000000000022e888000100f4b1b7c1237c118a43504f0b1f312c027fb7517cb46911d9a9ab1c432be0b039da618f84f833dc38b54f31cca63c408134bcce60f38c16c2f64fd4cf37068b0f205c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004da618f84f833dc38b54f31cca63c408134bcce60f38c16c2f64fd4cf37068b0f0753238a4817000000000000003ae888000100446ad8dfc37a54c8c1030e70eedd53b7b410cec1376a0aae42fd376b9749272f002326240a609c72641c6a3fcad3ad2ee33f2db95f6c16e181f7c62fa063834b245c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004002326240a609c72641c6a3fcad3ad2ee33f2db95f6c16e181f7c62fa063834b07fcbacd2e0200000000000000eec2890001008a90be061598f4b592afbd546bcb6beadb3c02f5c129df2e11b698f9543dbd412aa58acc7df6cea78de0928a5c6c2e79f3d24e5893d6a5971738cfbc03ca8f33285c010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000042aa58acc7df6cea78de0928a5c6c2e79f3d24e5893d6a5971738cfbc03ca8f330f65f486c4ba2401000000000000007a32c70001feff03000efdb10e8dfe2e73b6f29ce09aaad2c374dc22e2a64fcad4a77ed547e9175a08de74e52b42323230d99ec6f46fc1bf01b03f54f11f02fddb9d9668535e8ed4632c5c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004de74e52b42323230d99ec6f46fc1bf01b03f54f11f02fddb9d9668535e8ed46300000000000000002e0791000100984a52dfdfbba392625d238ff859ec78d79b04f8ad456c9ab8d8426a085d4e73de74e52b42323230d99ec6f46fc1bf01b03f54f11f02fddb9d9668535e8ed463305c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004de74e52b42323230d99ec6f46fc1bf01b03f54f11f02fddb9d9668535e8ed46307b7ec18d01f00000000000000720791000100c260f03b11e0e67574deb81febf89f6afb697d6c36d165f4ecb65522ea433805de74e52b42323230d99ec6f46fc1bf01b03f54f11f02fddb9d9668535e8ed463345c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004de74e52b42323230d99ec6f46fc1bf01b03f54f11f02fddb9d9668535e8ed4630000000000000000ae079100010024126de9392aa70486873b23182a00ee1b9e2f394a46d409230871113a76be56b88597476f139767a85863d096b7668620bcf7cb531a950006f18b26d4b30600385c00792d2b0000000000b60200004a024d940000000000000000000000009756040400000000000000000000000000000000000000000000000000000000000000000004b88597476f139767a85863d096b7668620bcf7cb531a950006f18b26d4b306000771d8c05f520000000000000046899400010062380cd8c9bbe606cfda6572b6da580ec035ec57b18dc3d64acbcef53c3efc71143906b4ea1ab0986976c98468b8371bd54cba3330c0e895a1762d8e96c12c023c5c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004143906b4ea1ab0986976c98468b8371bd54cba3330c0e895a1762d8e96c12c024a930c0000000000000000568da100010006c112ee93775bb184b6bf00f22adee24091efe725c249db3ba01b15274b2a2f9ef89253596e8764aecef9a452db36da8188153e0f707f1a0a37011583b5e82d405c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000049ef89253596e8764aecef9a452db36da8188153e0f707f1a0a37011583b5e82d07e68ca9693100000000000000d257ad000100e45433ff9820f9d62b98feff2a95281d14ff6028a9b3c78063a11bbd0fc7bd549ef89253596e8764aecef9a452db36da8188153e0f707f1a0a37011583b5e82d445c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000049ef89253596e8764aecef9a452db36da8188153e0f707f1a0a37011583b5e82d5a9c11a7000000000000004a59ad000100fa01f3e6d839007cc8179e6cb40fb87d4b87502315568340cc51255d4b526d59b24a20c37c76fe89b0ab49ef5ef0e5ceeb33964ef89a496944e9b31cb4915d1b485c004bc72d0000000000b60200008cbc4c94000000000000000000000000464a040400000000000000000000000000000000000000000000000000000000000000000004b24a20c37c76fe89b0ab49ef5ef0e5ceeb33964ef89a496944e9b31cb4915d1bd51500000000000000eedfb4000100", + } + }, + "state_getRuntimeVersion": { + '["0x03a008636efbe7706f79f2c7d7d4e85ff26a3e67922e5fefab71e7f6d9dab43d"]': { + "jsonrpc": "2.0", + "result": { + "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", 5], + ["0xe65b00e46cedd0aa", 2], + ["0x42e62be4a39e5b60", 1], + ["0x806df4ccaa9ed485", 1], + ["0x8375104b299b74c5", 1], + ["0x5d1fbfbe852f2807", 1], + ["0xc6886e2f8e598b0a", 1], + ], + "authoringVersion": 1, + "implName": "node-subtensor", + "implVersion": 1, + "specName": "node-subtensor", + "specVersion": 208, + "stateVersion": 1, + "transactionVersion": 1, + }, + } + }, + "state_getStorageAt": { + '["0xca407206ec1ab726b2636c4b145ac287419a60ae8b01e6dcaebd7317e43c69bf1700915e602fd97dd63a4af7598952f87683204a2a0c2266f941a74899bd69b0ce56e42a82820f689446", "0xa049374c707f9a426866a090c66eff7106a75597dd0d87a8fbe39a7f7887ed16"]': { + "jsonrpc": "2.0", + "result": None, + } + }, + "system_chain": {"[]": {"jsonrpc": "2.0", "result": "Bittensor"}}, + }, + "get_current_block": { + "chain_getHeader": { + "[null]": { + "jsonrpc": "2.0", + "result": { + "digest": { + "logs": [ + "0x0661757261209c489a0800000000", + "0x0466726f6e8801b81937c0aed82aace40c1860c8f8be871ed90466eb702dcd50fef49a70ca8dcf00", + "0x056175726101016a8bbee0a2b31058eff0df90b3b194cc7824e735e09b957136291639ff36c2047102e48742ac2ac14fe1634b652bba055b00383b06cea6482c56755b74c3c88b", + ] + }, + "extrinsicsRoot": "0x42acc4ffcaba39f003a14f13e2dc69e5b93198724a019515f31e22baf0b240f7", + "number": "0x31ce8f", + "parentHash": "0xe9729c54c0e59c611198b560a7a93e52154100187d04dfc09d1dc1a6572d4006", + "stateRoot": "0x319de5cb67bbf31e93a7db2e75b9bca48ce8d0b91d5156ce2c738eb178700d98", + }, + } + }, + "system_chain": {"[]": {"jsonrpc": "2.0", "result": "Bittensor"}}, + }, + "get_delegate_by_hotkey": { + "delegateInfo_getDelegate": { + "[[74, 247, 89, 137, 82, 248, 118, 131, 32, 74, 42, 12, 34, 102, 249, 65, 167, 72, 153, 189, 105, 176, 206, 86, 228, 42, 130, 130, 15, 104, 148, 70]]": { + "jsonrpc": "2.0", + "result": [ + 74, + 247, + 89, + 137, + 82, + 248, + 118, + 131, + 32, + 74, + 42, + 12, + 34, + 102, + 249, + 65, + 167, + 72, + 153, + 189, + 105, + 176, + 206, + 86, + 228, + 42, + 130, + 130, + 15, + 104, + 148, + 70, + 81, + 184, + 4, + 18, + 87, + 222, + 135, + 167, + 26, + 187, + 22, + 68, + 3, + 247, + 203, + 158, + 166, + 93, + 7, + 33, + 248, + 187, + 137, + 216, + 189, + 133, + 117, + 77, + 236, + 238, + 221, + 145, + 232, + 96, + 83, + 3, + 176, + 236, + 104, + 179, + 18, + 87, + 222, + 135, + 167, + 26, + 187, + 22, + 68, + 3, + 247, + 203, + 158, + 166, + 93, + 7, + 33, + 248, + 187, + 137, + 216, + 189, + 133, + 117, + 77, + 236, + 238, + 221, + 145, + 232, + 96, + 83, + 4, + 92, + 4, + 92, + 0, + 0, + ], + } + }, + "system_chain": {"[]": {"jsonrpc": "2.0", "result": "Bittensor"}}, + }, + "get_delegate_take": { + "chain_getHead": { + "[]": { + "jsonrpc": "2.0", + "result": "0x78fcbb9531d6f1032b78164d980f79994f4321d8bdd2e1446696db38b6be9906", + } + }, + "chain_getHeader": { + '["0x78fcbb9531d6f1032b78164d980f79994f4321d8bdd2e1446696db38b6be9906"]': { + "jsonrpc": "2.0", + "result": { + "digest": { + "logs": [ + "0x066175726120af489a0800000000", + "0x0466726f6e88019e6b8c3ee8e78fdae0e9837c9233a72d4dae4a78a502619f3136a9944dcfd31600", + "0x056175726101017ed9e3fadc29c95e5982ab7126e1274cabb953d101256ec0b99db2f4ce266d658ed03fd0ec8ed66c686874d3a668e3fe38562c3fd51e378e10a6f00dfa3b1f82", + ] + }, + "extrinsicsRoot": "0xbd59ea9aed82ecde226b70a90b38690562e7d0ef2e0bc8c6a70d297a39c0f4f3", + "number": "0x31cea2", + "parentHash": "0xef1f94e2a46fbd2b39f60d855da491ee6e2637d9c6092358f8ce97b6839973ae", + "stateRoot": "0xdfa23783ec2405ae41f8d17add8e1fc2c415042425d30f43411b8be6f37775b5", + }, + } + }, + "rpc_methods": { + "[]": { + "jsonrpc": "2.0", + "result": { + "methods": [ + "account_nextIndex", + "archive_unstable_body", + "archive_unstable_call", + "archive_unstable_finalizedHeight", + "archive_unstable_genesisHash", + "archive_unstable_hashByHeight", + "archive_unstable_header", + "archive_unstable_storage", + "author_hasKey", + "author_hasSessionKeys", + "author_insertKey", + "author_pendingExtrinsics", + "author_removeExtrinsic", + "author_rotateKeys", + "author_submitAndWatchExtrinsic", + "author_submitExtrinsic", + "author_unwatchExtrinsic", + "chainHead_v1_body", + "chainHead_v1_call", + "chainHead_v1_continue", + "chainHead_v1_follow", + "chainHead_v1_header", + "chainHead_v1_stopOperation", + "chainHead_v1_storage", + "chainHead_v1_unfollow", + "chainHead_v1_unpin", + "chainSpec_v1_chainName", + "chainSpec_v1_genesisHash", + "chainSpec_v1_properties", + "chain_getBlock", + "chain_getBlockHash", + "chain_getFinalisedHead", + "chain_getFinalizedHead", + "chain_getHead", + "chain_getHeader", + "chain_getRuntimeVersion", + "chain_subscribeAllHeads", + "chain_subscribeFinalisedHeads", + "chain_subscribeFinalizedHeads", + "chain_subscribeNewHead", + "chain_subscribeNewHeads", + "chain_subscribeRuntimeVersion", + "chain_unsubscribeAllHeads", + "chain_unsubscribeFinalisedHeads", + "chain_unsubscribeFinalizedHeads", + "chain_unsubscribeNewHead", + "chain_unsubscribeNewHeads", + "chain_unsubscribeRuntimeVersion", + "childstate_getKeys", + "childstate_getKeysPaged", + "childstate_getKeysPagedAt", + "childstate_getStorage", + "childstate_getStorageEntries", + "childstate_getStorageHash", + "childstate_getStorageSize", + "debug_getBadBlocks", + "debug_getRawBlock", + "debug_getRawHeader", + "debug_getRawReceipts", + "debug_getRawTransaction", + "delegateInfo_getDelegate", + "delegateInfo_getDelegated", + "delegateInfo_getDelegates", + "eth_accounts", + "eth_blockNumber", + "eth_call", + "eth_chainId", + "eth_coinbase", + "eth_estimateGas", + "eth_feeHistory", + "eth_gasPrice", + "eth_getBalance", + "eth_getBlockByHash", + "eth_getBlockByNumber", + "eth_getBlockReceipts", + "eth_getBlockTransactionCountByHash", + "eth_getBlockTransactionCountByNumber", + "eth_getCode", + "eth_getFilterChanges", + "eth_getFilterLogs", + "eth_getLogs", + "eth_getStorageAt", + "eth_getTransactionByBlockHashAndIndex", + "eth_getTransactionByBlockNumberAndIndex", + "eth_getTransactionByHash", + "eth_getTransactionCount", + "eth_getTransactionReceipt", + "eth_getUncleByBlockHashAndIndex", + "eth_getUncleByBlockNumberAndIndex", + "eth_getUncleCountByBlockHash", + "eth_getUncleCountByBlockNumber", + "eth_getWork", + "eth_hashrate", + "eth_maxPriorityFeePerGas", + "eth_mining", + "eth_newBlockFilter", + "eth_newFilter", + "eth_newPendingTransactionFilter", + "eth_protocolVersion", + "eth_sendRawTransaction", + "eth_sendTransaction", + "eth_submitHashrate", + "eth_submitWork", + "eth_subscribe", + "eth_syncing", + "eth_uninstallFilter", + "eth_unsubscribe", + "net_listening", + "net_peerCount", + "net_version", + "neuronInfo_getNeuron", + "neuronInfo_getNeuronLite", + "neuronInfo_getNeurons", + "neuronInfo_getNeuronsLite", + "offchain_localStorageGet", + "offchain_localStorageSet", + "payment_queryFeeDetails", + "payment_queryInfo", + "rpc_methods", + "state_call", + "state_callAt", + "state_getChildReadProof", + "state_getKeys", + "state_getKeysPaged", + "state_getKeysPagedAt", + "state_getMetadata", + "state_getPairs", + "state_getReadProof", + "state_getRuntimeVersion", + "state_getStorage", + "state_getStorageAt", + "state_getStorageHash", + "state_getStorageHashAt", + "state_getStorageSize", + "state_getStorageSizeAt", + "state_queryStorage", + "state_queryStorageAt", + "state_subscribeRuntimeVersion", + "state_subscribeStorage", + "state_traceBlock", + "state_unsubscribeRuntimeVersion", + "state_unsubscribeStorage", + "subnetInfo_getLockCost", + "subnetInfo_getSubnetHyperparams", + "subnetInfo_getSubnetInfo", + "subnetInfo_getSubnetInfo_v2", + "subnetInfo_getSubnetsInf_v2", + "subnetInfo_getSubnetsInfo", + "subscribe_newHead", + "system_accountNextIndex", + "system_addLogFilter", + "system_addReservedPeer", + "system_chain", + "system_chainType", + "system_dryRun", + "system_dryRunAt", + "system_health", + "system_localListenAddresses", + "system_localPeerId", + "system_name", + "system_nodeRoles", + "system_peers", + "system_properties", + "system_removeReservedPeer", + "system_reservedPeers", + "system_resetLogFilter", + "system_syncState", + "system_unstable_networkState", + "system_version", + "transactionWatch_v1_submitAndWatch", + "transactionWatch_v1_unwatch", + "transaction_v1_broadcast", + "transaction_v1_stop", + "unsubscribe_newHead", + "web3_clientVersion", + "web3_sha3", + ] + }, + } + }, + "state_getRuntimeVersion": { + '["0xef1f94e2a46fbd2b39f60d855da491ee6e2637d9c6092358f8ce97b6839973ae"]': { + "jsonrpc": "2.0", + "result": { + "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", 5], + ["0xe65b00e46cedd0aa", 2], + ["0x42e62be4a39e5b60", 1], + ["0x806df4ccaa9ed485", 1], + ["0x8375104b299b74c5", 1], + ["0x5d1fbfbe852f2807", 1], + ["0xc6886e2f8e598b0a", 1], + ], + "authoringVersion": 1, + "implName": "node-subtensor", + "implVersion": 1, + "specName": "node-subtensor", + "specVersion": 208, + "stateVersion": 1, + "transactionVersion": 1, + }, + } + }, + "state_getStorageAt": { + '["0x658faa385070e074c85bf6b568cf055560d1f0ff648e4c86ea413fc0173d40383e9947bb89c47cd7ad643c5d30c0e6954af7598952f87683204a2a0c2266f941a74899bd69b0ce56e42a82820f689446", "0x78fcbb9531d6f1032b78164d980f79994f4321d8bdd2e1446696db38b6be9906"]': { + "jsonrpc": "2.0", + "result": "0x142e", + } + }, + "system_chain": {"[]": {"jsonrpc": "2.0", "result": "Bittensor"}}, + }, + "get_existential_deposit": { + "chain_getHead": { + "[]": { + "jsonrpc": "2.0", + "result": "0xa0ee9e0d47444a8570eec6e4e48120bba7aef6fcce277d90cce6d48655398459", + } + }, + "chain_getHeader": { + '["0xa0ee9e0d47444a8570eec6e4e48120bba7aef6fcce277d90cce6d48655398459"]': { + "jsonrpc": "2.0", + "result": { + "digest": { + "logs": [ + "0x066175726120ad489a0800000000", + "0x0466726f6e8801d21c5c20a98fa8231a6942afda5eca4e92755efafca49d5ace36ca55071aec4500", + "0x056175726101015c4e2f637e25bf9306c9b3fc913b4e7b5236482bdda345af93123f247337796f5e99fe1f0521663d0231125bca06658222ea52aa66760b9395208287d595a78a", + ] + }, + "extrinsicsRoot": "0xe02b6ace11324334f7dd66ea5fe31f668f7e2b0889a945f48080b39273d9c321", + "number": "0x31cea0", + "parentHash": "0xcc5c5bb73bd11f8f74688c582a50859cefc1b49aee62055a4695ae89a1ae4471", + "stateRoot": "0x7844964f3bc43cb86f85e7d33cc69002fb17107904df939c1b485f3b76ca40c9", + }, + } + }, + "rpc_methods": { + "[]": { + "jsonrpc": "2.0", + "result": { + "methods": [ + "account_nextIndex", + "archive_unstable_body", + "archive_unstable_call", + "archive_unstable_finalizedHeight", + "archive_unstable_genesisHash", + "archive_unstable_hashByHeight", + "archive_unstable_header", + "archive_unstable_storage", + "author_hasKey", + "author_hasSessionKeys", + "author_insertKey", + "author_pendingExtrinsics", + "author_removeExtrinsic", + "author_rotateKeys", + "author_submitAndWatchExtrinsic", + "author_submitExtrinsic", + "author_unwatchExtrinsic", + "chainHead_v1_body", + "chainHead_v1_call", + "chainHead_v1_continue", + "chainHead_v1_follow", + "chainHead_v1_header", + "chainHead_v1_stopOperation", + "chainHead_v1_storage", + "chainHead_v1_unfollow", + "chainHead_v1_unpin", + "chainSpec_v1_chainName", + "chainSpec_v1_genesisHash", + "chainSpec_v1_properties", + "chain_getBlock", + "chain_getBlockHash", + "chain_getFinalisedHead", + "chain_getFinalizedHead", + "chain_getHead", + "chain_getHeader", + "chain_getRuntimeVersion", + "chain_subscribeAllHeads", + "chain_subscribeFinalisedHeads", + "chain_subscribeFinalizedHeads", + "chain_subscribeNewHead", + "chain_subscribeNewHeads", + "chain_subscribeRuntimeVersion", + "chain_unsubscribeAllHeads", + "chain_unsubscribeFinalisedHeads", + "chain_unsubscribeFinalizedHeads", + "chain_unsubscribeNewHead", + "chain_unsubscribeNewHeads", + "chain_unsubscribeRuntimeVersion", + "childstate_getKeys", + "childstate_getKeysPaged", + "childstate_getKeysPagedAt", + "childstate_getStorage", + "childstate_getStorageEntries", + "childstate_getStorageHash", + "childstate_getStorageSize", + "debug_getBadBlocks", + "debug_getRawBlock", + "debug_getRawHeader", + "debug_getRawReceipts", + "debug_getRawTransaction", + "delegateInfo_getDelegate", + "delegateInfo_getDelegated", + "delegateInfo_getDelegates", + "eth_accounts", + "eth_blockNumber", + "eth_call", + "eth_chainId", + "eth_coinbase", + "eth_estimateGas", + "eth_feeHistory", + "eth_gasPrice", + "eth_getBalance", + "eth_getBlockByHash", + "eth_getBlockByNumber", + "eth_getBlockReceipts", + "eth_getBlockTransactionCountByHash", + "eth_getBlockTransactionCountByNumber", + "eth_getCode", + "eth_getFilterChanges", + "eth_getFilterLogs", + "eth_getLogs", + "eth_getStorageAt", + "eth_getTransactionByBlockHashAndIndex", + "eth_getTransactionByBlockNumberAndIndex", + "eth_getTransactionByHash", + "eth_getTransactionCount", + "eth_getTransactionReceipt", + "eth_getUncleByBlockHashAndIndex", + "eth_getUncleByBlockNumberAndIndex", + "eth_getUncleCountByBlockHash", + "eth_getUncleCountByBlockNumber", + "eth_getWork", + "eth_hashrate", + "eth_maxPriorityFeePerGas", + "eth_mining", + "eth_newBlockFilter", + "eth_newFilter", + "eth_newPendingTransactionFilter", + "eth_protocolVersion", + "eth_sendRawTransaction", + "eth_sendTransaction", + "eth_submitHashrate", + "eth_submitWork", + "eth_subscribe", + "eth_syncing", + "eth_uninstallFilter", + "eth_unsubscribe", + "net_listening", + "net_peerCount", + "net_version", + "neuronInfo_getNeuron", + "neuronInfo_getNeuronLite", + "neuronInfo_getNeurons", + "neuronInfo_getNeuronsLite", + "offchain_localStorageGet", + "offchain_localStorageSet", + "payment_queryFeeDetails", + "payment_queryInfo", + "rpc_methods", + "state_call", + "state_callAt", + "state_getChildReadProof", + "state_getKeys", + "state_getKeysPaged", + "state_getKeysPagedAt", + "state_getMetadata", + "state_getPairs", + "state_getReadProof", + "state_getRuntimeVersion", + "state_getStorage", + "state_getStorageAt", + "state_getStorageHash", + "state_getStorageHashAt", + "state_getStorageSize", + "state_getStorageSizeAt", + "state_queryStorage", + "state_queryStorageAt", + "state_subscribeRuntimeVersion", + "state_subscribeStorage", + "state_traceBlock", + "state_unsubscribeRuntimeVersion", + "state_unsubscribeStorage", + "subnetInfo_getLockCost", + "subnetInfo_getSubnetHyperparams", + "subnetInfo_getSubnetInfo", + "subnetInfo_getSubnetInfo_v2", + "subnetInfo_getSubnetsInf_v2", + "subnetInfo_getSubnetsInfo", + "subscribe_newHead", + "system_accountNextIndex", + "system_addLogFilter", + "system_addReservedPeer", + "system_chain", + "system_chainType", + "system_dryRun", + "system_dryRunAt", + "system_health", + "system_localListenAddresses", + "system_localPeerId", + "system_name", + "system_nodeRoles", + "system_peers", + "system_properties", + "system_removeReservedPeer", + "system_reservedPeers", + "system_resetLogFilter", + "system_syncState", + "system_unstable_networkState", + "system_version", + "transactionWatch_v1_submitAndWatch", + "transactionWatch_v1_unwatch", + "transaction_v1_broadcast", + "transaction_v1_stop", + "unsubscribe_newHead", + "web3_clientVersion", + "web3_sha3", + ] + }, + } + }, + "state_getRuntimeVersion": { + '["0xcc5c5bb73bd11f8f74688c582a50859cefc1b49aee62055a4695ae89a1ae4471"]': { + "jsonrpc": "2.0", + "result": { + "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", 5], + ["0xe65b00e46cedd0aa", 2], + ["0x42e62be4a39e5b60", 1], + ["0x806df4ccaa9ed485", 1], + ["0x8375104b299b74c5", 1], + ["0x5d1fbfbe852f2807", 1], + ["0xc6886e2f8e598b0a", 1], + ], + "authoringVersion": 1, + "implName": "node-subtensor", + "implVersion": 1, + "specName": "node-subtensor", + "specVersion": 208, + "stateVersion": 1, + "transactionVersion": 1, + }, + } + }, + "system_chain": {"[]": {"jsonrpc": "2.0", "result": "Bittensor"}}, + }, + "get_netuids_for_hotkey": { + "chain_getHead": { + "[]": { + "jsonrpc": "2.0", + "result": "0x1e326794f8c851e3b5675e603bf726120b074348a6c0d3e8ea354f6880a49f7b", + } + }, + "chain_getHeader": { + '["0x1e326794f8c851e3b5675e603bf726120b074348a6c0d3e8ea354f6880a49f7b"]': { + "jsonrpc": "2.0", + "result": { + "digest": { + "logs": [ + "0x0661757261209c489a0800000000", + "0x0466726f6e8801b81937c0aed82aace40c1860c8f8be871ed90466eb702dcd50fef49a70ca8dcf00", + "0x056175726101016a8bbee0a2b31058eff0df90b3b194cc7824e735e09b957136291639ff36c2047102e48742ac2ac14fe1634b652bba055b00383b06cea6482c56755b74c3c88b", + ] + }, + "extrinsicsRoot": "0x42acc4ffcaba39f003a14f13e2dc69e5b93198724a019515f31e22baf0b240f7", + "number": "0x31ce8f", + "parentHash": "0xe9729c54c0e59c611198b560a7a93e52154100187d04dfc09d1dc1a6572d4006", + "stateRoot": "0x319de5cb67bbf31e93a7db2e75b9bca48ce8d0b91d5156ce2c738eb178700d98", + }, + } + }, + "rpc_methods": { + "[]": { + "jsonrpc": "2.0", + "result": { + "methods": [ + "account_nextIndex", + "archive_unstable_body", + "archive_unstable_call", + "archive_unstable_finalizedHeight", + "archive_unstable_genesisHash", + "archive_unstable_hashByHeight", + "archive_unstable_header", + "archive_unstable_storage", + "author_hasKey", + "author_hasSessionKeys", + "author_insertKey", + "author_pendingExtrinsics", + "author_removeExtrinsic", + "author_rotateKeys", + "author_submitAndWatchExtrinsic", + "author_submitExtrinsic", + "author_unwatchExtrinsic", + "chainHead_v1_body", + "chainHead_v1_call", + "chainHead_v1_continue", + "chainHead_v1_follow", + "chainHead_v1_header", + "chainHead_v1_stopOperation", + "chainHead_v1_storage", + "chainHead_v1_unfollow", + "chainHead_v1_unpin", + "chainSpec_v1_chainName", + "chainSpec_v1_genesisHash", + "chainSpec_v1_properties", + "chain_getBlock", + "chain_getBlockHash", + "chain_getFinalisedHead", + "chain_getFinalizedHead", + "chain_getHead", + "chain_getHeader", + "chain_getRuntimeVersion", + "chain_subscribeAllHeads", + "chain_subscribeFinalisedHeads", + "chain_subscribeFinalizedHeads", + "chain_subscribeNewHead", + "chain_subscribeNewHeads", + "chain_subscribeRuntimeVersion", + "chain_unsubscribeAllHeads", + "chain_unsubscribeFinalisedHeads", + "chain_unsubscribeFinalizedHeads", + "chain_unsubscribeNewHead", + "chain_unsubscribeNewHeads", + "chain_unsubscribeRuntimeVersion", + "childstate_getKeys", + "childstate_getKeysPaged", + "childstate_getKeysPagedAt", + "childstate_getStorage", + "childstate_getStorageEntries", + "childstate_getStorageHash", + "childstate_getStorageSize", + "debug_getBadBlocks", + "debug_getRawBlock", + "debug_getRawHeader", + "debug_getRawReceipts", + "debug_getRawTransaction", + "delegateInfo_getDelegate", + "delegateInfo_getDelegated", + "delegateInfo_getDelegates", + "eth_accounts", + "eth_blockNumber", + "eth_call", + "eth_chainId", + "eth_coinbase", + "eth_estimateGas", + "eth_feeHistory", + "eth_gasPrice", + "eth_getBalance", + "eth_getBlockByHash", + "eth_getBlockByNumber", + "eth_getBlockReceipts", + "eth_getBlockTransactionCountByHash", + "eth_getBlockTransactionCountByNumber", + "eth_getCode", + "eth_getFilterChanges", + "eth_getFilterLogs", + "eth_getLogs", + "eth_getStorageAt", + "eth_getTransactionByBlockHashAndIndex", + "eth_getTransactionByBlockNumberAndIndex", + "eth_getTransactionByHash", + "eth_getTransactionCount", + "eth_getTransactionReceipt", + "eth_getUncleByBlockHashAndIndex", + "eth_getUncleByBlockNumberAndIndex", + "eth_getUncleCountByBlockHash", + "eth_getUncleCountByBlockNumber", + "eth_getWork", + "eth_hashrate", + "eth_maxPriorityFeePerGas", + "eth_mining", + "eth_newBlockFilter", + "eth_newFilter", + "eth_newPendingTransactionFilter", + "eth_protocolVersion", + "eth_sendRawTransaction", + "eth_sendTransaction", + "eth_submitHashrate", + "eth_submitWork", + "eth_subscribe", + "eth_syncing", + "eth_uninstallFilter", + "eth_unsubscribe", + "net_listening", + "net_peerCount", + "net_version", + "neuronInfo_getNeuron", + "neuronInfo_getNeuronLite", + "neuronInfo_getNeurons", + "neuronInfo_getNeuronsLite", + "offchain_localStorageGet", + "offchain_localStorageSet", + "payment_queryFeeDetails", + "payment_queryInfo", + "rpc_methods", + "state_call", + "state_callAt", + "state_getChildReadProof", + "state_getKeys", + "state_getKeysPaged", + "state_getKeysPagedAt", + "state_getMetadata", + "state_getPairs", + "state_getReadProof", + "state_getRuntimeVersion", + "state_getStorage", + "state_getStorageAt", + "state_getStorageHash", + "state_getStorageHashAt", + "state_getStorageSize", + "state_getStorageSizeAt", + "state_queryStorage", + "state_queryStorageAt", + "state_subscribeRuntimeVersion", + "state_subscribeStorage", + "state_traceBlock", + "state_unsubscribeRuntimeVersion", + "state_unsubscribeStorage", + "subnetInfo_getLockCost", + "subnetInfo_getSubnetHyperparams", + "subnetInfo_getSubnetInfo", + "subnetInfo_getSubnetInfo_v2", + "subnetInfo_getSubnetsInf_v2", + "subnetInfo_getSubnetsInfo", + "subscribe_newHead", + "system_accountNextIndex", + "system_addLogFilter", + "system_addReservedPeer", + "system_chain", + "system_chainType", + "system_dryRun", + "system_dryRunAt", + "system_health", + "system_localListenAddresses", + "system_localPeerId", + "system_name", + "system_nodeRoles", + "system_peers", + "system_properties", + "system_removeReservedPeer", + "system_reservedPeers", + "system_resetLogFilter", + "system_syncState", + "system_unstable_networkState", + "system_version", + "transactionWatch_v1_submitAndWatch", + "transactionWatch_v1_unwatch", + "transaction_v1_broadcast", + "transaction_v1_stop", + "unsubscribe_newHead", + "web3_clientVersion", + "web3_sha3", + ] + }, + } + }, + "state_getKeysPaged": { + '["0x658faa385070e074c85bf6b568cf0555ea6aa4e81a33d2c120ef55203e0eb5603e9947bb89c47cd7ad643c5d30c0e6954af7598952f87683204a2a0c2266f941a74899bd69b0ce56e42a82820f689446", 100, "0x658faa385070e074c85bf6b568cf0555ea6aa4e81a33d2c120ef55203e0eb5603e9947bb89c47cd7ad643c5d30c0e6954af7598952f87683204a2a0c2266f941a74899bd69b0ce56e42a82820f689446", null]': { + "jsonrpc": "2.0", + "result": [ + "0x658faa385070e074c85bf6b568cf0555ea6aa4e81a33d2c120ef55203e0eb5603e9947bb89c47cd7ad643c5d30c0e6954af7598952f87683204a2a0c2266f941a74899bd69b0ce56e42a82820f6894461700" + ], + }, + '["0x658faa385070e074c85bf6b568cf0555ea6aa4e81a33d2c120ef55203e0eb5603e9947bb89c47cd7ad643c5d30c0e6954af7598952f87683204a2a0c2266f941a74899bd69b0ce56e42a82820f689446", 100, "0x658faa385070e074c85bf6b568cf0555ea6aa4e81a33d2c120ef55203e0eb5603e9947bb89c47cd7ad643c5d30c0e6954af7598952f87683204a2a0c2266f941a74899bd69b0ce56e42a82820f6894461700", null]': { + "jsonrpc": "2.0", + "result": [], + }, + }, + "state_getRuntimeVersion": { + '["0xe9729c54c0e59c611198b560a7a93e52154100187d04dfc09d1dc1a6572d4006"]': { + "jsonrpc": "2.0", + "result": { + "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", 5], + ["0xe65b00e46cedd0aa", 2], + ["0x42e62be4a39e5b60", 1], + ["0x806df4ccaa9ed485", 1], + ["0x8375104b299b74c5", 1], + ["0x5d1fbfbe852f2807", 1], + ["0xc6886e2f8e598b0a", 1], + ], + "authoringVersion": 1, + "implName": "node-subtensor", + "implVersion": 1, + "specName": "node-subtensor", + "specVersion": 208, + "stateVersion": 1, + "transactionVersion": 1, + }, + } + }, + "state_queryStorageAt": { + '[["0x658faa385070e074c85bf6b568cf0555ea6aa4e81a33d2c120ef55203e0eb5603e9947bb89c47cd7ad643c5d30c0e6954af7598952f87683204a2a0c2266f941a74899bd69b0ce56e42a82820f6894461700"], null]': { + "jsonrpc": "2.0", + "result": [ + { + "block": "0x1e326794f8c851e3b5675e603bf726120b074348a6c0d3e8ea354f6880a49f7b", + "changes": [ + [ + "0x658faa385070e074c85bf6b568cf0555ea6aa4e81a33d2c120ef55203e0eb5603e9947bb89c47cd7ad643c5d30c0e6954af7598952f87683204a2a0c2266f941a74899bd69b0ce56e42a82820f6894461700", + "0x01", + ] + ], + } + ], + } + }, + "system_chain": {"[]": {"jsonrpc": "2.0", "result": "Bittensor"}}, + }, + "get_neuron_for_pubkey_and_subnet": { + "chain_getHead": { + "[]": { + "jsonrpc": "2.0", + "result": "0x9d58a80049c3eaabf68f6bc83b1dd3f36bc3dcc7d75be17ee53dfc678e4ee111", + } + }, + "chain_getHeader": { + '["0x9d58a80049c3eaabf68f6bc83b1dd3f36bc3dcc7d75be17ee53dfc678e4ee111"]': { + "jsonrpc": "2.0", + "result": { + "digest": { + "logs": [ + "0x066175726120a0489a0800000000", + "0x0466726f6e8801b2557a32e7b78d386e67769c083405a148fe57d80528d9aca8eb9ddbbfac288800", + "0x05617572610101b8a3e510b65d00ebc479d3102632250e7354a619d7719757df6b0666c1eaa0039debd861b6ae79fcbc4f82b87d5efbc5ca089e318918e5274d0380782c91188b", + ] + }, + "extrinsicsRoot": "0x226f5c8e7d52bebaa5fb56a9a9f8344667533c2f9edac2eed9c91c64a9cc653c", + "number": "0x31ce93", + "parentHash": "0x9259b771b0f59b1ed8f38f604ab4f4a3e63dc0988754e664be2d40d58970958c", + "stateRoot": "0x8d8552df3fa7fb8569216cc69516dd3067af8d5bad60cf363c1337719bd31a12", + }, + } + }, + "state_call": { + '["NeuronInfoRuntimeApi_get_neuron", "01000500", null]': { + "jsonrpc": "2.0", + "result": "0x016a4368adc8781240b59c45ccacbf5a7ee0ca3a7adc7e893c27b4956e172c0220d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d040400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d0000000000000000361b010001000000", + }, + }, + "neuronInfo_getNeuron": { + "[23, 5]": { + "jsonrpc": "2.0", + "result": [ + 74, + 247, + 89, + 137, + 82, + 248, + 118, + 131, + 32, + 74, + 42, + 12, + 34, + 102, + 249, + 65, + 167, + 72, + 153, + 189, + 105, + 176, + 206, + 86, + 228, + 42, + 130, + 130, + 15, + 104, + 148, + 70, + 18, + 87, + 222, + 135, + 167, + 26, + 187, + 22, + 68, + 3, + 247, + 203, + 158, + 166, + 93, + 7, + 33, + 248, + 187, + 137, + 216, + 189, + 133, + 117, + 77, + 236, + 238, + 221, + 145, + 232, + 96, + 83, + 20, + 92, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 4, + 18, + 87, + 222, + 135, + 167, + 26, + 187, + 22, + 68, + 3, + 247, + 203, + 158, + 166, + 93, + 7, + 33, + 248, + 187, + 137, + 216, + 189, + 133, + 117, + 77, + 236, + 238, + 221, + 145, + 232, + 96, + 83, + 3, + 176, + 236, + 104, + 179, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 110, + 68, + 120, + 0, + 1, + 0, + 0, + 0, + ], + } + }, + "rpc_methods": { + "[]": { + "jsonrpc": "2.0", + "result": { + "methods": [ + "account_nextIndex", + "archive_unstable_body", + "archive_unstable_call", + "archive_unstable_finalizedHeight", + "archive_unstable_genesisHash", + "archive_unstable_hashByHeight", + "archive_unstable_header", + "archive_unstable_storage", + "author_hasKey", + "author_hasSessionKeys", + "author_insertKey", + "author_pendingExtrinsics", + "author_removeExtrinsic", + "author_rotateKeys", + "author_submitAndWatchExtrinsic", + "author_submitExtrinsic", + "author_unwatchExtrinsic", + "chainHead_v1_body", + "chainHead_v1_call", + "chainHead_v1_continue", + "chainHead_v1_follow", + "chainHead_v1_header", + "chainHead_v1_stopOperation", + "chainHead_v1_storage", + "chainHead_v1_unfollow", + "chainHead_v1_unpin", + "chainSpec_v1_chainName", + "chainSpec_v1_genesisHash", + "chainSpec_v1_properties", + "chain_getBlock", + "chain_getBlockHash", + "chain_getFinalisedHead", + "chain_getFinalizedHead", + "chain_getHead", + "chain_getHeader", + "chain_getRuntimeVersion", + "chain_subscribeAllHeads", + "chain_subscribeFinalisedHeads", + "chain_subscribeFinalizedHeads", + "chain_subscribeNewHead", + "chain_subscribeNewHeads", + "chain_subscribeRuntimeVersion", + "chain_unsubscribeAllHeads", + "chain_unsubscribeFinalisedHeads", + "chain_unsubscribeFinalizedHeads", + "chain_unsubscribeNewHead", + "chain_unsubscribeNewHeads", + "chain_unsubscribeRuntimeVersion", + "childstate_getKeys", + "childstate_getKeysPaged", + "childstate_getKeysPagedAt", + "childstate_getStorage", + "childstate_getStorageEntries", + "childstate_getStorageHash", + "childstate_getStorageSize", + "debug_getBadBlocks", + "debug_getRawBlock", + "debug_getRawHeader", + "debug_getRawReceipts", + "debug_getRawTransaction", + "delegateInfo_getDelegate", + "delegateInfo_getDelegated", + "delegateInfo_getDelegates", + "eth_accounts", + "eth_blockNumber", + "eth_call", + "eth_chainId", + "eth_coinbase", + "eth_estimateGas", + "eth_feeHistory", + "eth_gasPrice", + "eth_getBalance", + "eth_getBlockByHash", + "eth_getBlockByNumber", + "eth_getBlockReceipts", + "eth_getBlockTransactionCountByHash", + "eth_getBlockTransactionCountByNumber", + "eth_getCode", + "eth_getFilterChanges", + "eth_getFilterLogs", + "eth_getLogs", + "eth_getStorageAt", + "eth_getTransactionByBlockHashAndIndex", + "eth_getTransactionByBlockNumberAndIndex", + "eth_getTransactionByHash", + "eth_getTransactionCount", + "eth_getTransactionReceipt", + "eth_getUncleByBlockHashAndIndex", + "eth_getUncleByBlockNumberAndIndex", + "eth_getUncleCountByBlockHash", + "eth_getUncleCountByBlockNumber", + "eth_getWork", + "eth_hashrate", + "eth_maxPriorityFeePerGas", + "eth_mining", + "eth_newBlockFilter", + "eth_newFilter", + "eth_newPendingTransactionFilter", + "eth_protocolVersion", + "eth_sendRawTransaction", + "eth_sendTransaction", + "eth_submitHashrate", + "eth_submitWork", + "eth_subscribe", + "eth_syncing", + "eth_uninstallFilter", + "eth_unsubscribe", + "net_listening", + "net_peerCount", + "net_version", + "neuronInfo_getNeuron", + "neuronInfo_getNeuronLite", + "neuronInfo_getNeurons", + "neuronInfo_getNeuronsLite", + "offchain_localStorageGet", + "offchain_localStorageSet", + "payment_queryFeeDetails", + "payment_queryInfo", + "rpc_methods", + "state_call", + "state_callAt", + "state_getChildReadProof", + "state_getKeys", + "state_getKeysPaged", + "state_getKeysPagedAt", + "state_getMetadata", + "state_getPairs", + "state_getReadProof", + "state_getRuntimeVersion", + "state_getStorage", + "state_getStorageAt", + "state_getStorageHash", + "state_getStorageHashAt", + "state_getStorageSize", + "state_getStorageSizeAt", + "state_queryStorage", + "state_queryStorageAt", + "state_subscribeRuntimeVersion", + "state_subscribeStorage", + "state_traceBlock", + "state_unsubscribeRuntimeVersion", + "state_unsubscribeStorage", + "subnetInfo_getLockCost", + "subnetInfo_getSubnetHyperparams", + "subnetInfo_getSubnetInfo", + "subnetInfo_getSubnetInfo_v2", + "subnetInfo_getSubnetsInf_v2", + "subnetInfo_getSubnetsInfo", + "subscribe_newHead", + "system_accountNextIndex", + "system_addLogFilter", + "system_addReservedPeer", + "system_chain", + "system_chainType", + "system_dryRun", + "system_dryRunAt", + "system_health", + "system_localListenAddresses", + "system_localPeerId", + "system_name", + "system_nodeRoles", + "system_peers", + "system_properties", + "system_removeReservedPeer", + "system_reservedPeers", + "system_resetLogFilter", + "system_syncState", + "system_unstable_networkState", + "system_version", + "transactionWatch_v1_submitAndWatch", + "transactionWatch_v1_unwatch", + "transaction_v1_broadcast", + "transaction_v1_stop", + "unsubscribe_newHead", + "web3_clientVersion", + "web3_sha3", + ] + }, + } + }, + "state_getRuntimeVersion": { + '["0x9259b771b0f59b1ed8f38f604ab4f4a3e63dc0988754e664be2d40d58970958c"]': { + "jsonrpc": "2.0", + "result": { + "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", 5], + ["0xe65b00e46cedd0aa", 2], + ["0x42e62be4a39e5b60", 1], + ["0x806df4ccaa9ed485", 1], + ["0x8375104b299b74c5", 1], + ["0x5d1fbfbe852f2807", 1], + ["0xc6886e2f8e598b0a", 1], + ], + "authoringVersion": 1, + "implName": "node-subtensor", + "implVersion": 1, + "specName": "node-subtensor", + "specVersion": 208, + "stateVersion": 1, + "transactionVersion": 1, + }, + } + }, + "state_getStorageAt": { + '["0x658faa385070e074c85bf6b568cf0555aab1b4e78e1ea8305462ee53b3686dc801000107dcdbd6d7a9d789ac7a30a57647f66a4368adc8781240b59c45ccacbf5a7ee0ca3a7adc7e893c27b4956e172c0220", null]': { + "jsonrpc": "2.0", + "result": "0x0500", + } + }, + "system_chain": {"[]": {"jsonrpc": "2.0", "result": "Bittensor"}}, + }, + "get_prometheus_info": { + "chain_getHead": { + "[]": { + "jsonrpc": "2.0", + "result": "0xf305f795b37cb91dbe8369cf67c0badc41dae5c15811ecfdad5fc101f3cc17f5", + } + }, + "chain_getHeader": { + '["0xf305f795b37cb91dbe8369cf67c0badc41dae5c15811ecfdad5fc101f3cc17f5"]': { + "jsonrpc": "2.0", + "result": { + "digest": { + "logs": [ + "0x066175726120a6489a0800000000", + "0x0466726f6e8801e86843cec76184aecb7aaf69b884447f83148b115de587bea2db93e51ba4967c00", + "0x05617572610101b2ad5d9ff2e0abcfaae0f21b718b0eb00ec4f0d0ba10d64b39e2b626e27ea22e3a9d15e550ecc5db3de897d3108a1c73d5b003a7c5ddb8914bf9bf64ae95b58b", + ] + }, + "extrinsicsRoot": "0xdec7259ef64ad7fe645b11a2814ea1cdb56783399b7ae14f54838186c7ef7659", + "number": "0x31ce99", + "parentHash": "0x056fb97cc61bb3892f5c1f58574cd8a489ccfb7681079137b7bb2f7aaaaeef35", + "stateRoot": "0xd97a981b0fe489518abb7bd1d7375cb6f4505ea95e0a8c97e7e08013b19bc32c", + }, + } + }, + "rpc_methods": { + "[]": { + "jsonrpc": "2.0", + "result": { + "methods": [ + "account_nextIndex", + "archive_unstable_body", + "archive_unstable_call", + "archive_unstable_finalizedHeight", + "archive_unstable_genesisHash", + "archive_unstable_hashByHeight", + "archive_unstable_header", + "archive_unstable_storage", + "author_hasKey", + "author_hasSessionKeys", + "author_insertKey", + "author_pendingExtrinsics", + "author_removeExtrinsic", + "author_rotateKeys", + "author_submitAndWatchExtrinsic", + "author_submitExtrinsic", + "author_unwatchExtrinsic", + "chainHead_v1_body", + "chainHead_v1_call", + "chainHead_v1_continue", + "chainHead_v1_follow", + "chainHead_v1_header", + "chainHead_v1_stopOperation", + "chainHead_v1_storage", + "chainHead_v1_unfollow", + "chainHead_v1_unpin", + "chainSpec_v1_chainName", + "chainSpec_v1_genesisHash", + "chainSpec_v1_properties", + "chain_getBlock", + "chain_getBlockHash", + "chain_getFinalisedHead", + "chain_getFinalizedHead", + "chain_getHead", + "chain_getHeader", + "chain_getRuntimeVersion", + "chain_subscribeAllHeads", + "chain_subscribeFinalisedHeads", + "chain_subscribeFinalizedHeads", + "chain_subscribeNewHead", + "chain_subscribeNewHeads", + "chain_subscribeRuntimeVersion", + "chain_unsubscribeAllHeads", + "chain_unsubscribeFinalisedHeads", + "chain_unsubscribeFinalizedHeads", + "chain_unsubscribeNewHead", + "chain_unsubscribeNewHeads", + "chain_unsubscribeRuntimeVersion", + "childstate_getKeys", + "childstate_getKeysPaged", + "childstate_getKeysPagedAt", + "childstate_getStorage", + "childstate_getStorageEntries", + "childstate_getStorageHash", + "childstate_getStorageSize", + "debug_getBadBlocks", + "debug_getRawBlock", + "debug_getRawHeader", + "debug_getRawReceipts", + "debug_getRawTransaction", + "delegateInfo_getDelegate", + "delegateInfo_getDelegated", + "delegateInfo_getDelegates", + "eth_accounts", + "eth_blockNumber", + "eth_call", + "eth_chainId", + "eth_coinbase", + "eth_estimateGas", + "eth_feeHistory", + "eth_gasPrice", + "eth_getBalance", + "eth_getBlockByHash", + "eth_getBlockByNumber", + "eth_getBlockReceipts", + "eth_getBlockTransactionCountByHash", + "eth_getBlockTransactionCountByNumber", + "eth_getCode", + "eth_getFilterChanges", + "eth_getFilterLogs", + "eth_getLogs", + "eth_getStorageAt", + "eth_getTransactionByBlockHashAndIndex", + "eth_getTransactionByBlockNumberAndIndex", + "eth_getTransactionByHash", + "eth_getTransactionCount", + "eth_getTransactionReceipt", + "eth_getUncleByBlockHashAndIndex", + "eth_getUncleByBlockNumberAndIndex", + "eth_getUncleCountByBlockHash", + "eth_getUncleCountByBlockNumber", + "eth_getWork", + "eth_hashrate", + "eth_maxPriorityFeePerGas", + "eth_mining", + "eth_newBlockFilter", + "eth_newFilter", + "eth_newPendingTransactionFilter", + "eth_protocolVersion", + "eth_sendRawTransaction", + "eth_sendTransaction", + "eth_submitHashrate", + "eth_submitWork", + "eth_subscribe", + "eth_syncing", + "eth_uninstallFilter", + "eth_unsubscribe", + "net_listening", + "net_peerCount", + "net_version", + "neuronInfo_getNeuron", + "neuronInfo_getNeuronLite", + "neuronInfo_getNeurons", + "neuronInfo_getNeuronsLite", + "offchain_localStorageGet", + "offchain_localStorageSet", + "payment_queryFeeDetails", + "payment_queryInfo", + "rpc_methods", + "state_call", + "state_callAt", + "state_getChildReadProof", + "state_getKeys", + "state_getKeysPaged", + "state_getKeysPagedAt", + "state_getMetadata", + "state_getPairs", + "state_getReadProof", + "state_getRuntimeVersion", + "state_getStorage", + "state_getStorageAt", + "state_getStorageHash", + "state_getStorageHashAt", + "state_getStorageSize", + "state_getStorageSizeAt", + "state_queryStorage", + "state_queryStorageAt", + "state_subscribeRuntimeVersion", + "state_subscribeStorage", + "state_traceBlock", + "state_unsubscribeRuntimeVersion", + "state_unsubscribeStorage", + "subnetInfo_getLockCost", + "subnetInfo_getSubnetHyperparams", + "subnetInfo_getSubnetInfo", + "subnetInfo_getSubnetInfo_v2", + "subnetInfo_getSubnetsInf_v2", + "subnetInfo_getSubnetsInfo", + "subscribe_newHead", + "system_accountNextIndex", + "system_addLogFilter", + "system_addReservedPeer", + "system_chain", + "system_chainType", + "system_dryRun", + "system_dryRunAt", + "system_health", + "system_localListenAddresses", + "system_localPeerId", + "system_name", + "system_nodeRoles", + "system_peers", + "system_properties", + "system_removeReservedPeer", + "system_reservedPeers", + "system_resetLogFilter", + "system_syncState", + "system_unstable_networkState", + "system_version", + "transactionWatch_v1_submitAndWatch", + "transactionWatch_v1_unwatch", + "transaction_v1_broadcast", + "transaction_v1_stop", + "unsubscribe_newHead", + "web3_clientVersion", + "web3_sha3", + ] + }, + } + }, + "state_getRuntimeVersion": { + '["0x056fb97cc61bb3892f5c1f58574cd8a489ccfb7681079137b7bb2f7aaaaeef35"]': { + "jsonrpc": "2.0", + "result": { + "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", 5], + ["0xe65b00e46cedd0aa", 2], + ["0x42e62be4a39e5b60", 1], + ["0x806df4ccaa9ed485", 1], + ["0x8375104b299b74c5", 1], + ["0x5d1fbfbe852f2807", 1], + ["0xc6886e2f8e598b0a", 1], + ], + "authoringVersion": 1, + "implName": "node-subtensor", + "implVersion": 1, + "specName": "node-subtensor", + "specVersion": 208, + "stateVersion": 1, + "transactionVersion": 1, + }, + } + }, + "state_getStorageAt": { + '["0x658faa385070e074c85bf6b568cf0555e2cb8fd8f8e097862c6255753a3c788a17003e9947bb89c47cd7ad643c5d30c0e6954af7598952f87683204a2a0c2266f941a74899bd69b0ce56e42a82820f689446", "0xf305f795b37cb91dbe8369cf67c0badc41dae5c15811ecfdad5fc101f3cc17f5"]': { + "jsonrpc": "2.0", + "result": None, + } + }, + "system_chain": {"[]": {"jsonrpc": "2.0", "result": "Bittensor"}}, + }, + "get_subnet_burn_cost": { + "state_call": { + '["SubnetRegistrationRuntimeApi_get_network_registration_cost", "0x"]': { + "jsonrpc": "2.0", + "result": "0x40d48b5800000000", + } + }, + "system_chain": {"[]": {"jsonrpc": "2.0", "result": "Bittensor"}}, + }, + "get_subnet_hyperparameters": { + "chain_getHead": { + "[]": { + "jsonrpc": "2.0", + "result": "0x626a986aa4668a5df5cdac66ea726f442f97867e9ef3b5189388e2137c0e80f1", + } + }, + "chain_getHeader": { + '["0x626a986aa4668a5df5cdac66ea726f442f97867e9ef3b5189388e2137c0e80f1"]': { + "jsonrpc": "2.0", + "result": { + "digest": { + "logs": [ + "0x066175726120a1489a0800000000", + "0x0466726f6e8801010c46111723087aee56f995270845fd7f7c5e5eda88df43e55d2c322458d49800", + "0x05617572610101da727d00153aa042c155d0efb2c127a65267f68b52bc400230153c8ec2563c5c0438ef2e39d1c03a7f6f0a639b72eac7efb0a776ca55794d4baa793f98bc5d8e", + ] + }, + "extrinsicsRoot": "0x2ec9a61a1c1feff10f15835031611e191aec34b34bfe13d3107c985cf18ef453", + "number": "0x31ce94", + "parentHash": "0x9d58a80049c3eaabf68f6bc83b1dd3f36bc3dcc7d75be17ee53dfc678e4ee111", + "stateRoot": "0x793cc809def7ea8f56f5685fb363275b1169e7178c4ec42e90c66b5a9941c650", + }, + } + }, + "rpc_methods": { + "[]": { + "jsonrpc": "2.0", + "result": { + "methods": [ + "account_nextIndex", + "archive_unstable_body", + "archive_unstable_call", + "archive_unstable_finalizedHeight", + "archive_unstable_genesisHash", + "archive_unstable_hashByHeight", + "archive_unstable_header", + "archive_unstable_storage", + "author_hasKey", + "author_hasSessionKeys", + "author_insertKey", + "author_pendingExtrinsics", + "author_removeExtrinsic", + "author_rotateKeys", + "author_submitAndWatchExtrinsic", + "author_submitExtrinsic", + "author_unwatchExtrinsic", + "chainHead_v1_body", + "chainHead_v1_call", + "chainHead_v1_continue", + "chainHead_v1_follow", + "chainHead_v1_header", + "chainHead_v1_stopOperation", + "chainHead_v1_storage", + "chainHead_v1_unfollow", + "chainHead_v1_unpin", + "chainSpec_v1_chainName", + "chainSpec_v1_genesisHash", + "chainSpec_v1_properties", + "chain_getBlock", + "chain_getBlockHash", + "chain_getFinalisedHead", + "chain_getFinalizedHead", + "chain_getHead", + "chain_getHeader", + "chain_getRuntimeVersion", + "chain_subscribeAllHeads", + "chain_subscribeFinalisedHeads", + "chain_subscribeFinalizedHeads", + "chain_subscribeNewHead", + "chain_subscribeNewHeads", + "chain_subscribeRuntimeVersion", + "chain_unsubscribeAllHeads", + "chain_unsubscribeFinalisedHeads", + "chain_unsubscribeFinalizedHeads", + "chain_unsubscribeNewHead", + "chain_unsubscribeNewHeads", + "chain_unsubscribeRuntimeVersion", + "childstate_getKeys", + "childstate_getKeysPaged", + "childstate_getKeysPagedAt", + "childstate_getStorage", + "childstate_getStorageEntries", + "childstate_getStorageHash", + "childstate_getStorageSize", + "debug_getBadBlocks", + "debug_getRawBlock", + "debug_getRawHeader", + "debug_getRawReceipts", + "debug_getRawTransaction", + "delegateInfo_getDelegate", + "delegateInfo_getDelegated", + "delegateInfo_getDelegates", + "eth_accounts", + "eth_blockNumber", + "eth_call", + "eth_chainId", + "eth_coinbase", + "eth_estimateGas", + "eth_feeHistory", + "eth_gasPrice", + "eth_getBalance", + "eth_getBlockByHash", + "eth_getBlockByNumber", + "eth_getBlockReceipts", + "eth_getBlockTransactionCountByHash", + "eth_getBlockTransactionCountByNumber", + "eth_getCode", + "eth_getFilterChanges", + "eth_getFilterLogs", + "eth_getLogs", + "eth_getStorageAt", + "eth_getTransactionByBlockHashAndIndex", + "eth_getTransactionByBlockNumberAndIndex", + "eth_getTransactionByHash", + "eth_getTransactionCount", + "eth_getTransactionReceipt", + "eth_getUncleByBlockHashAndIndex", + "eth_getUncleByBlockNumberAndIndex", + "eth_getUncleCountByBlockHash", + "eth_getUncleCountByBlockNumber", + "eth_getWork", + "eth_hashrate", + "eth_maxPriorityFeePerGas", + "eth_mining", + "eth_newBlockFilter", + "eth_newFilter", + "eth_newPendingTransactionFilter", + "eth_protocolVersion", + "eth_sendRawTransaction", + "eth_sendTransaction", + "eth_submitHashrate", + "eth_submitWork", + "eth_subscribe", + "eth_syncing", + "eth_uninstallFilter", + "eth_unsubscribe", + "net_listening", + "net_peerCount", + "net_version", + "neuronInfo_getNeuron", + "neuronInfo_getNeuronLite", + "neuronInfo_getNeurons", + "neuronInfo_getNeuronsLite", + "offchain_localStorageGet", + "offchain_localStorageSet", + "payment_queryFeeDetails", + "payment_queryInfo", + "rpc_methods", + "state_call", + "state_callAt", + "state_getChildReadProof", + "state_getKeys", + "state_getKeysPaged", + "state_getKeysPagedAt", + "state_getMetadata", + "state_getPairs", + "state_getReadProof", + "state_getRuntimeVersion", + "state_getStorage", + "state_getStorageAt", + "state_getStorageHash", + "state_getStorageHashAt", + "state_getStorageSize", + "state_getStorageSizeAt", + "state_queryStorage", + "state_queryStorageAt", + "state_subscribeRuntimeVersion", + "state_subscribeStorage", + "state_traceBlock", + "state_unsubscribeRuntimeVersion", + "state_unsubscribeStorage", + "subnetInfo_getLockCost", + "subnetInfo_getSubnetHyperparams", + "subnetInfo_getSubnetInfo", + "subnetInfo_getSubnetInfo_v2", + "subnetInfo_getSubnetsInf_v2", + "subnetInfo_getSubnetsInfo", + "subscribe_newHead", + "system_accountNextIndex", + "system_addLogFilter", + "system_addReservedPeer", + "system_chain", + "system_chainType", + "system_dryRun", + "system_dryRunAt", + "system_health", + "system_localListenAddresses", + "system_localPeerId", + "system_name", + "system_nodeRoles", + "system_peers", + "system_properties", + "system_removeReservedPeer", + "system_reservedPeers", + "system_resetLogFilter", + "system_syncState", + "system_unstable_networkState", + "system_version", + "transactionWatch_v1_submitAndWatch", + "transactionWatch_v1_unwatch", + "transaction_v1_broadcast", + "transaction_v1_stop", + "unsubscribe_newHead", + "web3_clientVersion", + "web3_sha3", + ] + }, + } + }, + "state_call": { + '["SubnetInfoRuntimeApi_get_subnet_hyperparams", "0x1700"]': { + "jsonrpc": "2.0", + "result": "0x190128feff0100214e04feff0300a10f025a620213ffffffffffffff3f009101d107214e0104040700e876481782ee360004c80101428a0300025a620204009a990300cecc020000", + } + }, + "state_getRuntimeVersion": { + '["0x9d58a80049c3eaabf68f6bc83b1dd3f36bc3dcc7d75be17ee53dfc678e4ee111"]': { + "jsonrpc": "2.0", + "result": { + "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", 5], + ["0xe65b00e46cedd0aa", 2], + ["0x42e62be4a39e5b60", 1], + ["0x806df4ccaa9ed485", 1], + ["0x8375104b299b74c5", 1], + ["0x5d1fbfbe852f2807", 1], + ["0xc6886e2f8e598b0a", 1], + ], + "authoringVersion": 1, + "implName": "node-subtensor", + "implVersion": 1, + "specName": "node-subtensor", + "specVersion": 208, + "stateVersion": 1, + "transactionVersion": 1, + }, + } + }, + "system_chain": {"[]": {"jsonrpc": "2.0", "result": "Bittensor"}}, + }, + "get_subnets": { + "chain_getHead": { + "[]": { + "jsonrpc": "2.0", + "result": "0xd867f39bb9e2a626945183af01065b61d0695d435a765bfd0ce5f821fbed8f82", + } + }, + "chain_getHeader": { + '["0xd867f39bb9e2a626945183af01065b61d0695d435a765bfd0ce5f821fbed8f82"]': { + "jsonrpc": "2.0", + "result": { + "digest": { + "logs": [ + "0x066175726120aa489a0800000000", + "0x0466726f6e88011a69331481627271f5f40c4f35ebffac418255ef2257753a01e865193b2c4ee700", + "0x056175726101016edac58b21702011943c42db257af0026b7952343e03c341979d3d9414cd970e4e07a26708f8a6e66e9a46d3f440b40988a6ccdef3d8329fd7399faaa20cb185", + ] + }, + "extrinsicsRoot": "0xdb0d49424c84fec28943ea69ecdc5325f40f9c798a844c6e1e2313cf385d8571", + "number": "0x31ce9d", + "parentHash": "0x747c46b08af3b7bbfb04647b4befdf7be9c58946e1a01b171af76bdc5d37cce6", + "stateRoot": "0x161979ee1f3ab18ab09843502bdc05d43185465ac6008eb6cecc2104de848bf5", + }, + } + }, + "rpc_methods": { + "[]": { + "jsonrpc": "2.0", + "result": { + "methods": [ + "account_nextIndex", + "archive_unstable_body", + "archive_unstable_call", + "archive_unstable_finalizedHeight", + "archive_unstable_genesisHash", + "archive_unstable_hashByHeight", + "archive_unstable_header", + "archive_unstable_storage", + "author_hasKey", + "author_hasSessionKeys", + "author_insertKey", + "author_pendingExtrinsics", + "author_removeExtrinsic", + "author_rotateKeys", + "author_submitAndWatchExtrinsic", + "author_submitExtrinsic", + "author_unwatchExtrinsic", + "chainHead_v1_body", + "chainHead_v1_call", + "chainHead_v1_continue", + "chainHead_v1_follow", + "chainHead_v1_header", + "chainHead_v1_stopOperation", + "chainHead_v1_storage", + "chainHead_v1_unfollow", + "chainHead_v1_unpin", + "chainSpec_v1_chainName", + "chainSpec_v1_genesisHash", + "chainSpec_v1_properties", + "chain_getBlock", + "chain_getBlockHash", + "chain_getFinalisedHead", + "chain_getFinalizedHead", + "chain_getHead", + "chain_getHeader", + "chain_getRuntimeVersion", + "chain_subscribeAllHeads", + "chain_subscribeFinalisedHeads", + "chain_subscribeFinalizedHeads", + "chain_subscribeNewHead", + "chain_subscribeNewHeads", + "chain_subscribeRuntimeVersion", + "chain_unsubscribeAllHeads", + "chain_unsubscribeFinalisedHeads", + "chain_unsubscribeFinalizedHeads", + "chain_unsubscribeNewHead", + "chain_unsubscribeNewHeads", + "chain_unsubscribeRuntimeVersion", + "childstate_getKeys", + "childstate_getKeysPaged", + "childstate_getKeysPagedAt", + "childstate_getStorage", + "childstate_getStorageEntries", + "childstate_getStorageHash", + "childstate_getStorageSize", + "debug_getBadBlocks", + "debug_getRawBlock", + "debug_getRawHeader", + "debug_getRawReceipts", + "debug_getRawTransaction", + "delegateInfo_getDelegate", + "delegateInfo_getDelegated", + "delegateInfo_getDelegates", + "eth_accounts", + "eth_blockNumber", + "eth_call", + "eth_chainId", + "eth_coinbase", + "eth_estimateGas", + "eth_feeHistory", + "eth_gasPrice", + "eth_getBalance", + "eth_getBlockByHash", + "eth_getBlockByNumber", + "eth_getBlockReceipts", + "eth_getBlockTransactionCountByHash", + "eth_getBlockTransactionCountByNumber", + "eth_getCode", + "eth_getFilterChanges", + "eth_getFilterLogs", + "eth_getLogs", + "eth_getStorageAt", + "eth_getTransactionByBlockHashAndIndex", + "eth_getTransactionByBlockNumberAndIndex", + "eth_getTransactionByHash", + "eth_getTransactionCount", + "eth_getTransactionReceipt", + "eth_getUncleByBlockHashAndIndex", + "eth_getUncleByBlockNumberAndIndex", + "eth_getUncleCountByBlockHash", + "eth_getUncleCountByBlockNumber", + "eth_getWork", + "eth_hashrate", + "eth_maxPriorityFeePerGas", + "eth_mining", + "eth_newBlockFilter", + "eth_newFilter", + "eth_newPendingTransactionFilter", + "eth_protocolVersion", + "eth_sendRawTransaction", + "eth_sendTransaction", + "eth_submitHashrate", + "eth_submitWork", + "eth_subscribe", + "eth_syncing", + "eth_uninstallFilter", + "eth_unsubscribe", + "net_listening", + "net_peerCount", + "net_version", + "neuronInfo_getNeuron", + "neuronInfo_getNeuronLite", + "neuronInfo_getNeurons", + "neuronInfo_getNeuronsLite", + "offchain_localStorageGet", + "offchain_localStorageSet", + "payment_queryFeeDetails", + "payment_queryInfo", + "rpc_methods", + "state_call", + "state_callAt", + "state_getChildReadProof", + "state_getKeys", + "state_getKeysPaged", + "state_getKeysPagedAt", + "state_getMetadata", + "state_getPairs", + "state_getReadProof", + "state_getRuntimeVersion", + "state_getStorage", + "state_getStorageAt", + "state_getStorageHash", + "state_getStorageHashAt", + "state_getStorageSize", + "state_getStorageSizeAt", + "state_queryStorage", + "state_queryStorageAt", + "state_subscribeRuntimeVersion", + "state_subscribeStorage", + "state_traceBlock", + "state_unsubscribeRuntimeVersion", + "state_unsubscribeStorage", + "subnetInfo_getLockCost", + "subnetInfo_getSubnetHyperparams", + "subnetInfo_getSubnetInfo", + "subnetInfo_getSubnetInfo_v2", + "subnetInfo_getSubnetsInf_v2", + "subnetInfo_getSubnetsInfo", + "subscribe_newHead", + "system_accountNextIndex", + "system_addLogFilter", + "system_addReservedPeer", + "system_chain", + "system_chainType", + "system_dryRun", + "system_dryRunAt", + "system_health", + "system_localListenAddresses", + "system_localPeerId", + "system_name", + "system_nodeRoles", + "system_peers", + "system_properties", + "system_removeReservedPeer", + "system_reservedPeers", + "system_resetLogFilter", + "system_syncState", + "system_unstable_networkState", + "system_version", + "transactionWatch_v1_submitAndWatch", + "transactionWatch_v1_unwatch", + "transaction_v1_broadcast", + "transaction_v1_stop", + "unsubscribe_newHead", + "web3_clientVersion", + "web3_sha3", + ] + }, + } + }, + "state_getKeysPaged": { + '["0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a43", 100, "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a43", "0xd867f39bb9e2a626945183af01065b61d0695d435a765bfd0ce5f821fbed8f82"]': { + "jsonrpc": "2.0", + "result": [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a430000", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a430100", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a430200", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a430300", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a430400", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a430500", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a430600", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a430700", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a430800", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a430900", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a430a00", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a430b00", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a430c00", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a430d00", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a430e00", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a430f00", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a431000", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a431100", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a431200", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a431300", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a431400", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a431500", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a431600", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a431700", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a431800", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a431900", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a431a00", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a431b00", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a431c00", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a431d00", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a431e00", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a431f00", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a432000", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a432100", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a432200", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a432300", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a432400", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a432500", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a432600", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a432700", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a432800", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a432900", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a432a00", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a432b00", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a432c00", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a432d00", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a432e00", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a432f00", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a433000", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a433100", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a433200", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a433300", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a433400", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a433500", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a433600", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a433700", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a433800", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a433900", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a433a00", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a433b00", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a433c00", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a433d00", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a433e00", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a433f00", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a434000", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a434100", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a434200", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a434300", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a434400", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a434500", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a434600", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a434700", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a434800", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a434900", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a434a00", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a434b00", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a434c00", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a434d00", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a434e00", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a434f00", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a435000", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a435100", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a435200", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a435300", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a435400", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a435500", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a435600", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a435700", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a435800", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a435900", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a435a00", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a435b00", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a435c00", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a435d00", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a435e00", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a435f00", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a436000", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a436100", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a436200", + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a436300", + ], + } + }, + "state_getRuntimeVersion": { + '["0x747c46b08af3b7bbfb04647b4befdf7be9c58946e1a01b171af76bdc5d37cce6"]': { + "jsonrpc": "2.0", + "result": { + "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", 5], + ["0xe65b00e46cedd0aa", 2], + ["0x42e62be4a39e5b60", 1], + ["0x806df4ccaa9ed485", 1], + ["0x8375104b299b74c5", 1], + ["0x5d1fbfbe852f2807", 1], + ["0xc6886e2f8e598b0a", 1], + ], + "authoringVersion": 1, + "implName": "node-subtensor", + "implVersion": 1, + "specName": "node-subtensor", + "specVersion": 208, + "stateVersion": 1, + "transactionVersion": 1, + }, + } + }, + "state_queryStorageAt": { + '[["0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a430000", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a430100", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a430200", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a430300", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a430400", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a430500", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a430600", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a430700", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a430800", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a430900", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a430a00", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a430b00", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a430c00", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a430d00", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a430e00", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a430f00", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a431000", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a431100", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a431200", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a431300", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a431400", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a431500", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a431600", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a431700", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a431800", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a431900", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a431a00", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a431b00", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a431c00", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a431d00", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a431e00", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a431f00", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a432000", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a432100", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a432200", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a432300", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a432400", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a432500", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a432600", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a432700", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a432800", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a432900", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a432a00", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a432b00", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a432c00", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a432d00", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a432e00", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a432f00", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a433000", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a433100", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a433200", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a433300", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a433400", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a433500", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a433600", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a433700", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a433800", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a433900", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a433a00", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a433b00", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a433c00", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a433d00", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a433e00", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a433f00", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a434000", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a434100", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a434200", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a434300", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a434400", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a434500", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a434600", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a434700", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a434800", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a434900", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a434a00", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a434b00", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a434c00", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a434d00", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a434e00", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a434f00", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a435000", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a435100", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a435200", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a435300", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a435400", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a435500", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a435600", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a435700", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a435800", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a435900", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a435a00", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a435b00", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a435c00", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a435d00", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a435e00", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a435f00", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a436000", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a436100", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a436200", "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a436300"], "0xd867f39bb9e2a626945183af01065b61d0695d435a765bfd0ce5f821fbed8f82"]': { + "jsonrpc": "2.0", + "result": [ + { + "block": "0xd867f39bb9e2a626945183af01065b61d0695d435a765bfd0ce5f821fbed8f82", + "changes": [ + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a430000", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a430100", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a430200", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a430300", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a430400", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a430500", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a430600", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a430700", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a430800", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a430900", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a430a00", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a430b00", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a430c00", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a430d00", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a430e00", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a430f00", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a431000", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a431100", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a431200", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a431300", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a431400", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a431500", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a431600", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a431700", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a431800", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a431900", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a431a00", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a431b00", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a431c00", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a431d00", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a431e00", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a431f00", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a432000", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a432100", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a432200", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a432300", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a432400", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a432500", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a432600", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a432700", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a432800", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a432900", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a432a00", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a432b00", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a432c00", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a432d00", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a432e00", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a432f00", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a433000", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a433100", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a433200", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a433300", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a433400", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a433500", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a433600", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a433700", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a433800", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a433900", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a433a00", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a433b00", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a433c00", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a433d00", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a433e00", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a433f00", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a434000", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a434100", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a434200", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a434300", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a434400", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a434500", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a434600", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a434700", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a434800", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a434900", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a434a00", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a434b00", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a434c00", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a434d00", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a434e00", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a434f00", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a435000", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a435100", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a435200", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a435300", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a435400", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a435500", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a435600", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a435700", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a435800", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a435900", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a435a00", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a435b00", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a435c00", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a435d00", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a435e00", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a435f00", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a436000", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a436100", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a436200", + "0x01", + ], + [ + "0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a436300", + "0x01", + ], + ], + } + ], + } + }, + "system_chain": {"[]": {"jsonrpc": "2.0", "result": "Bittensor"}}, + }, + "get_total_subnets": { + "chain_getHead": { + "[]": { + "jsonrpc": "2.0", + "result": "0x747c46b08af3b7bbfb04647b4befdf7be9c58946e1a01b171af76bdc5d37cce6", + } + }, + "chain_getHeader": { + '["0x747c46b08af3b7bbfb04647b4befdf7be9c58946e1a01b171af76bdc5d37cce6"]': { + "jsonrpc": "2.0", + "result": { + "digest": { + "logs": [ + "0x066175726120a9489a0800000000", + "0x0466726f6e8801b95cd20cae558f77b52bec97040fe30e43283fd8e6762e612a238cd7ce60fa9400", + "0x05617572610101ec524faed02a7e4e36f5bfda3278bbe9b01737d80fb2083ec413c3575095951bdc2351f10348484cda77d8f7da00b8d23fe100732623a9d2aae659422cb5888a", + ] + }, + "extrinsicsRoot": "0xc407703dcfbe36ec23ce316c0c7d8d0b3db02c97359e70400c28e64dbdf95910", + "number": "0x31ce9c", + "parentHash": "0x67adc1cae2bb149f9cd5042966bb0ee02978dd600fcb64edca67c4cfa2650cc7", + "stateRoot": "0x9a9734fc313a151d000a910f34d3c538a42c6d5b9043d49acfdc15b48439690c", + }, + } + }, + "rpc_methods": { + "[]": { + "jsonrpc": "2.0", + "result": { + "methods": [ + "account_nextIndex", + "archive_unstable_body", + "archive_unstable_call", + "archive_unstable_finalizedHeight", + "archive_unstable_genesisHash", + "archive_unstable_hashByHeight", + "archive_unstable_header", + "archive_unstable_storage", + "author_hasKey", + "author_hasSessionKeys", + "author_insertKey", + "author_pendingExtrinsics", + "author_removeExtrinsic", + "author_rotateKeys", + "author_submitAndWatchExtrinsic", + "author_submitExtrinsic", + "author_unwatchExtrinsic", + "chainHead_v1_body", + "chainHead_v1_call", + "chainHead_v1_continue", + "chainHead_v1_follow", + "chainHead_v1_header", + "chainHead_v1_stopOperation", + "chainHead_v1_storage", + "chainHead_v1_unfollow", + "chainHead_v1_unpin", + "chainSpec_v1_chainName", + "chainSpec_v1_genesisHash", + "chainSpec_v1_properties", + "chain_getBlock", + "chain_getBlockHash", + "chain_getFinalisedHead", + "chain_getFinalizedHead", + "chain_getHead", + "chain_getHeader", + "chain_getRuntimeVersion", + "chain_subscribeAllHeads", + "chain_subscribeFinalisedHeads", + "chain_subscribeFinalizedHeads", + "chain_subscribeNewHead", + "chain_subscribeNewHeads", + "chain_subscribeRuntimeVersion", + "chain_unsubscribeAllHeads", + "chain_unsubscribeFinalisedHeads", + "chain_unsubscribeFinalizedHeads", + "chain_unsubscribeNewHead", + "chain_unsubscribeNewHeads", + "chain_unsubscribeRuntimeVersion", + "childstate_getKeys", + "childstate_getKeysPaged", + "childstate_getKeysPagedAt", + "childstate_getStorage", + "childstate_getStorageEntries", + "childstate_getStorageHash", + "childstate_getStorageSize", + "debug_getBadBlocks", + "debug_getRawBlock", + "debug_getRawHeader", + "debug_getRawReceipts", + "debug_getRawTransaction", + "delegateInfo_getDelegate", + "delegateInfo_getDelegated", + "delegateInfo_getDelegates", + "eth_accounts", + "eth_blockNumber", + "eth_call", + "eth_chainId", + "eth_coinbase", + "eth_estimateGas", + "eth_feeHistory", + "eth_gasPrice", + "eth_getBalance", + "eth_getBlockByHash", + "eth_getBlockByNumber", + "eth_getBlockReceipts", + "eth_getBlockTransactionCountByHash", + "eth_getBlockTransactionCountByNumber", + "eth_getCode", + "eth_getFilterChanges", + "eth_getFilterLogs", + "eth_getLogs", + "eth_getStorageAt", + "eth_getTransactionByBlockHashAndIndex", + "eth_getTransactionByBlockNumberAndIndex", + "eth_getTransactionByHash", + "eth_getTransactionCount", + "eth_getTransactionReceipt", + "eth_getUncleByBlockHashAndIndex", + "eth_getUncleByBlockNumberAndIndex", + "eth_getUncleCountByBlockHash", + "eth_getUncleCountByBlockNumber", + "eth_getWork", + "eth_hashrate", + "eth_maxPriorityFeePerGas", + "eth_mining", + "eth_newBlockFilter", + "eth_newFilter", + "eth_newPendingTransactionFilter", + "eth_protocolVersion", + "eth_sendRawTransaction", + "eth_sendTransaction", + "eth_submitHashrate", + "eth_submitWork", + "eth_subscribe", + "eth_syncing", + "eth_uninstallFilter", + "eth_unsubscribe", + "net_listening", + "net_peerCount", + "net_version", + "neuronInfo_getNeuron", + "neuronInfo_getNeuronLite", + "neuronInfo_getNeurons", + "neuronInfo_getNeuronsLite", + "offchain_localStorageGet", + "offchain_localStorageSet", + "payment_queryFeeDetails", + "payment_queryInfo", + "rpc_methods", + "state_call", + "state_callAt", + "state_getChildReadProof", + "state_getKeys", + "state_getKeysPaged", + "state_getKeysPagedAt", + "state_getMetadata", + "state_getPairs", + "state_getReadProof", + "state_getRuntimeVersion", + "state_getStorage", + "state_getStorageAt", + "state_getStorageHash", + "state_getStorageHashAt", + "state_getStorageSize", + "state_getStorageSizeAt", + "state_queryStorage", + "state_queryStorageAt", + "state_subscribeRuntimeVersion", + "state_subscribeStorage", + "state_traceBlock", + "state_unsubscribeRuntimeVersion", + "state_unsubscribeStorage", + "subnetInfo_getLockCost", + "subnetInfo_getSubnetHyperparams", + "subnetInfo_getSubnetInfo", + "subnetInfo_getSubnetInfo_v2", + "subnetInfo_getSubnetsInf_v2", + "subnetInfo_getSubnetsInfo", + "subscribe_newHead", + "system_accountNextIndex", + "system_addLogFilter", + "system_addReservedPeer", + "system_chain", + "system_chainType", + "system_dryRun", + "system_dryRunAt", + "system_health", + "system_localListenAddresses", + "system_localPeerId", + "system_name", + "system_nodeRoles", + "system_peers", + "system_properties", + "system_removeReservedPeer", + "system_reservedPeers", + "system_resetLogFilter", + "system_syncState", + "system_unstable_networkState", + "system_version", + "transactionWatch_v1_submitAndWatch", + "transactionWatch_v1_unwatch", + "transaction_v1_broadcast", + "transaction_v1_stop", + "unsubscribe_newHead", + "web3_clientVersion", + "web3_sha3", + ] + }, + } + }, + "state_getRuntimeVersion": { + '["0x67adc1cae2bb149f9cd5042966bb0ee02978dd600fcb64edca67c4cfa2650cc7"]': { + "jsonrpc": "2.0", + "result": { + "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", 5], + ["0xe65b00e46cedd0aa", 2], + ["0x42e62be4a39e5b60", 1], + ["0x806df4ccaa9ed485", 1], + ["0x8375104b299b74c5", 1], + ["0x5d1fbfbe852f2807", 1], + ["0xc6886e2f8e598b0a", 1], + ], + "authoringVersion": 1, + "implName": "node-subtensor", + "implVersion": 1, + "specName": "node-subtensor", + "specVersion": 208, + "stateVersion": 1, + "transactionVersion": 1, + }, + } + }, + "state_getStorageAt": { + '["0x658faa385070e074c85bf6b568cf05555f3bb7bcd0a076a48abf8c256d221721", "0x747c46b08af3b7bbfb04647b4befdf7be9c58946e1a01b171af76bdc5d37cce6"]': { + "jsonrpc": "2.0", + "result": "0xf700", + } + }, + "system_chain": {"[]": {"jsonrpc": "2.0", "result": "Bittensor"}}, + }, + "get_transfer_fee": { + "chain_getHead": { + "[]": { + "jsonrpc": "2.0", + "result": "0xcc5c5bb73bd11f8f74688c582a50859cefc1b49aee62055a4695ae89a1ae4471", + } + }, + "chain_getHeader": { + '["0xcc5c5bb73bd11f8f74688c582a50859cefc1b49aee62055a4695ae89a1ae4471"]': { + "jsonrpc": "2.0", + "result": { + "digest": { + "logs": [ + "0x066175726120ac489a0800000000", + "0x0466726f6e88011227aeb2b80157d35c15d0d8b9db0b229ce69d921d0fa4b6d04ffc83e9d2241c00", + "0x056175726101012628274835ec67ba3de42551d239cffa813cb170c0c6eae997e28b4a706b85235c58fb368dda142dae0fb1a7bc40146dfae62f9e5ef85e9d1b70d61cdc702f87", + ] + }, + "extrinsicsRoot": "0x86d419fcfc3c8b5c5f756d1fa18bb00b1b68127210770e383340add4de5322ba", + "number": "0x31ce9f", + "parentHash": "0x1b3f85dc8c83cada6d1782c955973a509a37c62af3d6ba29f5fd816aee37eb3b", + "stateRoot": "0xbfc75b1ed500d2e1534c13a191084e095940c7d15dadf727551356113e17936d", + }, + } + }, + "rpc_methods": { + "[]": { + "jsonrpc": "2.0", + "result": { + "methods": [ + "account_nextIndex", + "archive_unstable_body", + "archive_unstable_call", + "archive_unstable_finalizedHeight", + "archive_unstable_genesisHash", + "archive_unstable_hashByHeight", + "archive_unstable_header", + "archive_unstable_storage", + "author_hasKey", + "author_hasSessionKeys", + "author_insertKey", + "author_pendingExtrinsics", + "author_removeExtrinsic", + "author_rotateKeys", + "author_submitAndWatchExtrinsic", + "author_submitExtrinsic", + "author_unwatchExtrinsic", + "chainHead_v1_body", + "chainHead_v1_call", + "chainHead_v1_continue", + "chainHead_v1_follow", + "chainHead_v1_header", + "chainHead_v1_stopOperation", + "chainHead_v1_storage", + "chainHead_v1_unfollow", + "chainHead_v1_unpin", + "chainSpec_v1_chainName", + "chainSpec_v1_genesisHash", + "chainSpec_v1_properties", + "chain_getBlock", + "chain_getBlockHash", + "chain_getFinalisedHead", + "chain_getFinalizedHead", + "chain_getHead", + "chain_getHeader", + "chain_getRuntimeVersion", + "chain_subscribeAllHeads", + "chain_subscribeFinalisedHeads", + "chain_subscribeFinalizedHeads", + "chain_subscribeNewHead", + "chain_subscribeNewHeads", + "chain_subscribeRuntimeVersion", + "chain_unsubscribeAllHeads", + "chain_unsubscribeFinalisedHeads", + "chain_unsubscribeFinalizedHeads", + "chain_unsubscribeNewHead", + "chain_unsubscribeNewHeads", + "chain_unsubscribeRuntimeVersion", + "childstate_getKeys", + "childstate_getKeysPaged", + "childstate_getKeysPagedAt", + "childstate_getStorage", + "childstate_getStorageEntries", + "childstate_getStorageHash", + "childstate_getStorageSize", + "debug_getBadBlocks", + "debug_getRawBlock", + "debug_getRawHeader", + "debug_getRawReceipts", + "debug_getRawTransaction", + "delegateInfo_getDelegate", + "delegateInfo_getDelegated", + "delegateInfo_getDelegates", + "eth_accounts", + "eth_blockNumber", + "eth_call", + "eth_chainId", + "eth_coinbase", + "eth_estimateGas", + "eth_feeHistory", + "eth_gasPrice", + "eth_getBalance", + "eth_getBlockByHash", + "eth_getBlockByNumber", + "eth_getBlockReceipts", + "eth_getBlockTransactionCountByHash", + "eth_getBlockTransactionCountByNumber", + "eth_getCode", + "eth_getFilterChanges", + "eth_getFilterLogs", + "eth_getLogs", + "eth_getStorageAt", + "eth_getTransactionByBlockHashAndIndex", + "eth_getTransactionByBlockNumberAndIndex", + "eth_getTransactionByHash", + "eth_getTransactionCount", + "eth_getTransactionReceipt", + "eth_getUncleByBlockHashAndIndex", + "eth_getUncleByBlockNumberAndIndex", + "eth_getUncleCountByBlockHash", + "eth_getUncleCountByBlockNumber", + "eth_getWork", + "eth_hashrate", + "eth_maxPriorityFeePerGas", + "eth_mining", + "eth_newBlockFilter", + "eth_newFilter", + "eth_newPendingTransactionFilter", + "eth_protocolVersion", + "eth_sendRawTransaction", + "eth_sendTransaction", + "eth_submitHashrate", + "eth_submitWork", + "eth_subscribe", + "eth_syncing", + "eth_uninstallFilter", + "eth_unsubscribe", + "net_listening", + "net_peerCount", + "net_version", + "neuronInfo_getNeuron", + "neuronInfo_getNeuronLite", + "neuronInfo_getNeurons", + "neuronInfo_getNeuronsLite", + "offchain_localStorageGet", + "offchain_localStorageSet", + "payment_queryFeeDetails", + "payment_queryInfo", + "rpc_methods", + "state_call", + "state_callAt", + "state_getChildReadProof", + "state_getKeys", + "state_getKeysPaged", + "state_getKeysPagedAt", + "state_getMetadata", + "state_getPairs", + "state_getReadProof", + "state_getRuntimeVersion", + "state_getStorage", + "state_getStorageAt", + "state_getStorageHash", + "state_getStorageHashAt", + "state_getStorageSize", + "state_getStorageSizeAt", + "state_queryStorage", + "state_queryStorageAt", + "state_subscribeRuntimeVersion", + "state_subscribeStorage", + "state_traceBlock", + "state_unsubscribeRuntimeVersion", + "state_unsubscribeStorage", + "subnetInfo_getLockCost", + "subnetInfo_getSubnetHyperparams", + "subnetInfo_getSubnetInfo", + "subnetInfo_getSubnetInfo_v2", + "subnetInfo_getSubnetsInf_v2", + "subnetInfo_getSubnetsInfo", + "subscribe_newHead", + "system_accountNextIndex", + "system_addLogFilter", + "system_addReservedPeer", + "system_chain", + "system_chainType", + "system_dryRun", + "system_dryRunAt", + "system_health", + "system_localListenAddresses", + "system_localPeerId", + "system_name", + "system_nodeRoles", + "system_peers", + "system_properties", + "system_removeReservedPeer", + "system_reservedPeers", + "system_resetLogFilter", + "system_syncState", + "system_unstable_networkState", + "system_version", + "transactionWatch_v1_submitAndWatch", + "transactionWatch_v1_unwatch", + "transaction_v1_broadcast", + "transaction_v1_stop", + "unsubscribe_newHead", + "web3_clientVersion", + "web3_sha3", + ] + }, + } + }, + "state_getRuntimeVersion": { + '["0x1b3f85dc8c83cada6d1782c955973a509a37c62af3d6ba29f5fd816aee37eb3b"]': { + "jsonrpc": "2.0", + "result": { + "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", 5], + ["0xe65b00e46cedd0aa", 2], + ["0x42e62be4a39e5b60", 1], + ["0x806df4ccaa9ed485", 1], + ["0x8375104b299b74c5", 1], + ["0x5d1fbfbe852f2807", 1], + ["0xc6886e2f8e598b0a", 1], + ], + "authoringVersion": 1, + "implName": "node-subtensor", + "implVersion": 1, + "specName": "node-subtensor", + "specVersion": 208, + "stateVersion": 1, + "transactionVersion": 1, + }, + } + }, + "system_chain": {"[]": {"jsonrpc": "2.0", "result": "Bittensor"}}, + }, + "get_uid_for_hotkey_on_subnet": { + "chain_getHead": { + "[]": { + "jsonrpc": "2.0", + "result": "0x8d3cb34e83a45c0d2a2896bbe5fd34e108d9d82b7261a4eaf1c4b585c17a4689", + } + }, + "chain_getHeader": { + '["0x8d3cb34e83a45c0d2a2896bbe5fd34e108d9d82b7261a4eaf1c4b585c17a4689"]': { + "jsonrpc": "2.0", + "result": { + "digest": { + "logs": [ + "0x066175726120a2489a0800000000", + "0x0466726f6e880142bddc481d29c280be4f2c2712e63fbb0c03854c3bc97440ca6c94e031bbbd3c00", + "0x0561757261010140e6a9dcbd3a8e411820aabdd111706028afcae9b1ed1b532b6df20cfb6a145d2b6c45a6697470cc3a76415771e7c892b13f71dd5943f9da37399b8dfef12783", + ] + }, + "extrinsicsRoot": "0xbd2d4636f1dfd2f8294635225066d785d3806941a57bb48be3514436897522e0", + "number": "0x31ce95", + "parentHash": "0x626a986aa4668a5df5cdac66ea726f442f97867e9ef3b5189388e2137c0e80f1", + "stateRoot": "0xd2b2b4a516f44e01e2916412e2152ee9a44f9a72669ef9c23fe823f6b5443d8c", + }, + } + }, + "rpc_methods": { + "[]": { + "jsonrpc": "2.0", + "result": { + "methods": [ + "account_nextIndex", + "archive_unstable_body", + "archive_unstable_call", + "archive_unstable_finalizedHeight", + "archive_unstable_genesisHash", + "archive_unstable_hashByHeight", + "archive_unstable_header", + "archive_unstable_storage", + "author_hasKey", + "author_hasSessionKeys", + "author_insertKey", + "author_pendingExtrinsics", + "author_removeExtrinsic", + "author_rotateKeys", + "author_submitAndWatchExtrinsic", + "author_submitExtrinsic", + "author_unwatchExtrinsic", + "chainHead_v1_body", + "chainHead_v1_call", + "chainHead_v1_continue", + "chainHead_v1_follow", + "chainHead_v1_header", + "chainHead_v1_stopOperation", + "chainHead_v1_storage", + "chainHead_v1_unfollow", + "chainHead_v1_unpin", + "chainSpec_v1_chainName", + "chainSpec_v1_genesisHash", + "chainSpec_v1_properties", + "chain_getBlock", + "chain_getBlockHash", + "chain_getFinalisedHead", + "chain_getFinalizedHead", + "chain_getHead", + "chain_getHeader", + "chain_getRuntimeVersion", + "chain_subscribeAllHeads", + "chain_subscribeFinalisedHeads", + "chain_subscribeFinalizedHeads", + "chain_subscribeNewHead", + "chain_subscribeNewHeads", + "chain_subscribeRuntimeVersion", + "chain_unsubscribeAllHeads", + "chain_unsubscribeFinalisedHeads", + "chain_unsubscribeFinalizedHeads", + "chain_unsubscribeNewHead", + "chain_unsubscribeNewHeads", + "chain_unsubscribeRuntimeVersion", + "childstate_getKeys", + "childstate_getKeysPaged", + "childstate_getKeysPagedAt", + "childstate_getStorage", + "childstate_getStorageEntries", + "childstate_getStorageHash", + "childstate_getStorageSize", + "debug_getBadBlocks", + "debug_getRawBlock", + "debug_getRawHeader", + "debug_getRawReceipts", + "debug_getRawTransaction", + "delegateInfo_getDelegate", + "delegateInfo_getDelegated", + "delegateInfo_getDelegates", + "eth_accounts", + "eth_blockNumber", + "eth_call", + "eth_chainId", + "eth_coinbase", + "eth_estimateGas", + "eth_feeHistory", + "eth_gasPrice", + "eth_getBalance", + "eth_getBlockByHash", + "eth_getBlockByNumber", + "eth_getBlockReceipts", + "eth_getBlockTransactionCountByHash", + "eth_getBlockTransactionCountByNumber", + "eth_getCode", + "eth_getFilterChanges", + "eth_getFilterLogs", + "eth_getLogs", + "eth_getStorageAt", + "eth_getTransactionByBlockHashAndIndex", + "eth_getTransactionByBlockNumberAndIndex", + "eth_getTransactionByHash", + "eth_getTransactionCount", + "eth_getTransactionReceipt", + "eth_getUncleByBlockHashAndIndex", + "eth_getUncleByBlockNumberAndIndex", + "eth_getUncleCountByBlockHash", + "eth_getUncleCountByBlockNumber", + "eth_getWork", + "eth_hashrate", + "eth_maxPriorityFeePerGas", + "eth_mining", + "eth_newBlockFilter", + "eth_newFilter", + "eth_newPendingTransactionFilter", + "eth_protocolVersion", + "eth_sendRawTransaction", + "eth_sendTransaction", + "eth_submitHashrate", + "eth_submitWork", + "eth_subscribe", + "eth_syncing", + "eth_uninstallFilter", + "eth_unsubscribe", + "net_listening", + "net_peerCount", + "net_version", + "neuronInfo_getNeuron", + "neuronInfo_getNeuronLite", + "neuronInfo_getNeurons", + "neuronInfo_getNeuronsLite", + "offchain_localStorageGet", + "offchain_localStorageSet", + "payment_queryFeeDetails", + "payment_queryInfo", + "rpc_methods", + "state_call", + "state_callAt", + "state_getChildReadProof", + "state_getKeys", + "state_getKeysPaged", + "state_getKeysPagedAt", + "state_getMetadata", + "state_getPairs", + "state_getReadProof", + "state_getRuntimeVersion", + "state_getStorage", + "state_getStorageAt", + "state_getStorageHash", + "state_getStorageHashAt", + "state_getStorageSize", + "state_getStorageSizeAt", + "state_queryStorage", + "state_queryStorageAt", + "state_subscribeRuntimeVersion", + "state_subscribeStorage", + "state_traceBlock", + "state_unsubscribeRuntimeVersion", + "state_unsubscribeStorage", + "subnetInfo_getLockCost", + "subnetInfo_getSubnetHyperparams", + "subnetInfo_getSubnetInfo", + "subnetInfo_getSubnetInfo_v2", + "subnetInfo_getSubnetsInf_v2", + "subnetInfo_getSubnetsInfo", + "subscribe_newHead", + "system_accountNextIndex", + "system_addLogFilter", + "system_addReservedPeer", + "system_chain", + "system_chainType", + "system_dryRun", + "system_dryRunAt", + "system_health", + "system_localListenAddresses", + "system_localPeerId", + "system_name", + "system_nodeRoles", + "system_peers", + "system_properties", + "system_removeReservedPeer", + "system_reservedPeers", + "system_resetLogFilter", + "system_syncState", + "system_unstable_networkState", + "system_version", + "transactionWatch_v1_submitAndWatch", + "transactionWatch_v1_unwatch", + "transaction_v1_broadcast", + "transaction_v1_stop", + "unsubscribe_newHead", + "web3_clientVersion", + "web3_sha3", + ] + }, + } + }, + "state_getRuntimeVersion": { + '["0x626a986aa4668a5df5cdac66ea726f442f97867e9ef3b5189388e2137c0e80f1"]': { + "jsonrpc": "2.0", + "result": { + "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", 5], + ["0xe65b00e46cedd0aa", 2], + ["0x42e62be4a39e5b60", 1], + ["0x806df4ccaa9ed485", 1], + ["0x8375104b299b74c5", 1], + ["0x5d1fbfbe852f2807", 1], + ["0xc6886e2f8e598b0a", 1], + ], + "authoringVersion": 1, + "implName": "node-subtensor", + "implVersion": 1, + "specName": "node-subtensor", + "specVersion": 208, + "stateVersion": 1, + "transactionVersion": 1, + }, + } + }, + "state_getStorageAt": { + '["0x658faa385070e074c85bf6b568cf0555aab1b4e78e1ea8305462ee53b3686dc817003e9947bb89c47cd7ad643c5d30c0e6954af7598952f87683204a2a0c2266f941a74899bd69b0ce56e42a82820f689446", null]': { + "jsonrpc": "2.0", + "result": "0x0500", + } + }, + "system_chain": {"[]": {"jsonrpc": "2.0", "result": "Bittensor"}}, + }, + "immunity_period": { + "chain_getHead": { + "[]": { + "jsonrpc": "2.0", + "result": "0x8d3cb34e83a45c0d2a2896bbe5fd34e108d9d82b7261a4eaf1c4b585c17a4689", + } + }, + "chain_getHeader": { + '["0x8d3cb34e83a45c0d2a2896bbe5fd34e108d9d82b7261a4eaf1c4b585c17a4689"]': { + "jsonrpc": "2.0", + "result": { + "digest": { + "logs": [ + "0x066175726120a2489a0800000000", + "0x0466726f6e880142bddc481d29c280be4f2c2712e63fbb0c03854c3bc97440ca6c94e031bbbd3c00", + "0x0561757261010140e6a9dcbd3a8e411820aabdd111706028afcae9b1ed1b532b6df20cfb6a145d2b6c45a6697470cc3a76415771e7c892b13f71dd5943f9da37399b8dfef12783", + ] + }, + "extrinsicsRoot": "0xbd2d4636f1dfd2f8294635225066d785d3806941a57bb48be3514436897522e0", + "number": "0x31ce95", + "parentHash": "0x626a986aa4668a5df5cdac66ea726f442f97867e9ef3b5189388e2137c0e80f1", + "stateRoot": "0xd2b2b4a516f44e01e2916412e2152ee9a44f9a72669ef9c23fe823f6b5443d8c", + }, + } + }, + "rpc_methods": { + "[]": { + "jsonrpc": "2.0", + "result": { + "methods": [ + "account_nextIndex", + "archive_unstable_body", + "archive_unstable_call", + "archive_unstable_finalizedHeight", + "archive_unstable_genesisHash", + "archive_unstable_hashByHeight", + "archive_unstable_header", + "archive_unstable_storage", + "author_hasKey", + "author_hasSessionKeys", + "author_insertKey", + "author_pendingExtrinsics", + "author_removeExtrinsic", + "author_rotateKeys", + "author_submitAndWatchExtrinsic", + "author_submitExtrinsic", + "author_unwatchExtrinsic", + "chainHead_v1_body", + "chainHead_v1_call", + "chainHead_v1_continue", + "chainHead_v1_follow", + "chainHead_v1_header", + "chainHead_v1_stopOperation", + "chainHead_v1_storage", + "chainHead_v1_unfollow", + "chainHead_v1_unpin", + "chainSpec_v1_chainName", + "chainSpec_v1_genesisHash", + "chainSpec_v1_properties", + "chain_getBlock", + "chain_getBlockHash", + "chain_getFinalisedHead", + "chain_getFinalizedHead", + "chain_getHead", + "chain_getHeader", + "chain_getRuntimeVersion", + "chain_subscribeAllHeads", + "chain_subscribeFinalisedHeads", + "chain_subscribeFinalizedHeads", + "chain_subscribeNewHead", + "chain_subscribeNewHeads", + "chain_subscribeRuntimeVersion", + "chain_unsubscribeAllHeads", + "chain_unsubscribeFinalisedHeads", + "chain_unsubscribeFinalizedHeads", + "chain_unsubscribeNewHead", + "chain_unsubscribeNewHeads", + "chain_unsubscribeRuntimeVersion", + "childstate_getKeys", + "childstate_getKeysPaged", + "childstate_getKeysPagedAt", + "childstate_getStorage", + "childstate_getStorageEntries", + "childstate_getStorageHash", + "childstate_getStorageSize", + "debug_getBadBlocks", + "debug_getRawBlock", + "debug_getRawHeader", + "debug_getRawReceipts", + "debug_getRawTransaction", + "delegateInfo_getDelegate", + "delegateInfo_getDelegated", + "delegateInfo_getDelegates", + "eth_accounts", + "eth_blockNumber", + "eth_call", + "eth_chainId", + "eth_coinbase", + "eth_estimateGas", + "eth_feeHistory", + "eth_gasPrice", + "eth_getBalance", + "eth_getBlockByHash", + "eth_getBlockByNumber", + "eth_getBlockReceipts", + "eth_getBlockTransactionCountByHash", + "eth_getBlockTransactionCountByNumber", + "eth_getCode", + "eth_getFilterChanges", + "eth_getFilterLogs", + "eth_getLogs", + "eth_getStorageAt", + "eth_getTransactionByBlockHashAndIndex", + "eth_getTransactionByBlockNumberAndIndex", + "eth_getTransactionByHash", + "eth_getTransactionCount", + "eth_getTransactionReceipt", + "eth_getUncleByBlockHashAndIndex", + "eth_getUncleByBlockNumberAndIndex", + "eth_getUncleCountByBlockHash", + "eth_getUncleCountByBlockNumber", + "eth_getWork", + "eth_hashrate", + "eth_maxPriorityFeePerGas", + "eth_mining", + "eth_newBlockFilter", + "eth_newFilter", + "eth_newPendingTransactionFilter", + "eth_protocolVersion", + "eth_sendRawTransaction", + "eth_sendTransaction", + "eth_submitHashrate", + "eth_submitWork", + "eth_subscribe", + "eth_syncing", + "eth_uninstallFilter", + "eth_unsubscribe", + "net_listening", + "net_peerCount", + "net_version", + "neuronInfo_getNeuron", + "neuronInfo_getNeuronLite", + "neuronInfo_getNeurons", + "neuronInfo_getNeuronsLite", + "offchain_localStorageGet", + "offchain_localStorageSet", + "payment_queryFeeDetails", + "payment_queryInfo", + "rpc_methods", + "state_call", + "state_callAt", + "state_getChildReadProof", + "state_getKeys", + "state_getKeysPaged", + "state_getKeysPagedAt", + "state_getMetadata", + "state_getPairs", + "state_getReadProof", + "state_getRuntimeVersion", + "state_getStorage", + "state_getStorageAt", + "state_getStorageHash", + "state_getStorageHashAt", + "state_getStorageSize", + "state_getStorageSizeAt", + "state_queryStorage", + "state_queryStorageAt", + "state_subscribeRuntimeVersion", + "state_subscribeStorage", + "state_traceBlock", + "state_unsubscribeRuntimeVersion", + "state_unsubscribeStorage", + "subnetInfo_getLockCost", + "subnetInfo_getSubnetHyperparams", + "subnetInfo_getSubnetInfo", + "subnetInfo_getSubnetInfo_v2", + "subnetInfo_getSubnetsInf_v2", + "subnetInfo_getSubnetsInfo", + "subscribe_newHead", + "system_accountNextIndex", + "system_addLogFilter", + "system_addReservedPeer", + "system_chain", + "system_chainType", + "system_dryRun", + "system_dryRunAt", + "system_health", + "system_localListenAddresses", + "system_localPeerId", + "system_name", + "system_nodeRoles", + "system_peers", + "system_properties", + "system_removeReservedPeer", + "system_reservedPeers", + "system_resetLogFilter", + "system_syncState", + "system_unstable_networkState", + "system_version", + "transactionWatch_v1_submitAndWatch", + "transactionWatch_v1_unwatch", + "transaction_v1_broadcast", + "transaction_v1_stop", + "unsubscribe_newHead", + "web3_clientVersion", + "web3_sha3", + ] + }, + } + }, + "state_getRuntimeVersion": { + '["0x626a986aa4668a5df5cdac66ea726f442f97867e9ef3b5189388e2137c0e80f1"]': { + "jsonrpc": "2.0", + "result": { + "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", 5], + ["0xe65b00e46cedd0aa", 2], + ["0x42e62be4a39e5b60", 1], + ["0x806df4ccaa9ed485", 1], + ["0x8375104b299b74c5", 1], + ["0x5d1fbfbe852f2807", 1], + ["0xc6886e2f8e598b0a", 1], + ], + "authoringVersion": 1, + "implName": "node-subtensor", + "implVersion": 1, + "specName": "node-subtensor", + "specVersion": 208, + "stateVersion": 1, + "transactionVersion": 1, + }, + } + }, + "state_getStorageAt": { + '["0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a431700", "0x8d3cb34e83a45c0d2a2896bbe5fd34e108d9d82b7261a4eaf1c4b585c17a4689"]': { + "jsonrpc": "2.0", + "result": "0x01", + }, + '["0x658faa385070e074c85bf6b568cf0555b6522cfe03433e9e101a258ee2f580ab1700", "0x8d3cb34e83a45c0d2a2896bbe5fd34e108d9d82b7261a4eaf1c4b585c17a4689"]': { + "jsonrpc": "2.0", + "result": "0x8813", + }, + }, + "system_chain": {"[]": {"jsonrpc": "2.0", "result": "Bittensor"}}, + }, + "is_hotkey_registered": { + "chain_getHead": { + "[]": { + "jsonrpc": "2.0", + "result": "0x119e135f8ae6eac1f3fc47fc14838afcfdbee13e8dba14657ae150f4d56f6df2", + } + }, + "chain_getHeader": { + '["0x119e135f8ae6eac1f3fc47fc14838afcfdbee13e8dba14657ae150f4d56f6df2"]': { + "jsonrpc": "2.0", + "result": { + "digest": { + "logs": [ + "0x0661757261209e489a0800000000", + "0x0466726f6e880102c9d38834b1cbf58314661a6a0e2275b1e8f92e5dd8718795e9ae79ed74604f00", + "0x05617572610101361d04d1d191de564333a2da77ac19a1bcbcdd7bf197c8a6f9971b0d37818c7f2d2720030a0522f21e5aec6774d99a5a4916dede752cddc758f62d0d600cd98b", + ] + }, + "extrinsicsRoot": "0x2421536e7e6f4637a93134af1724955027d79f3a1c026df946d0bd91894c1ffa", + "number": "0x31ce91", + "parentHash": "0xdec7c1cfe134265d3bcb2b5c5b35ba6a08eedf29717fbce446d99e940bff72b5", + "stateRoot": "0x85d317d80126462107e1fb5588579465979aa302dac7a7577e34b94e941c4040", + }, + } + }, + "rpc_methods": { + "[]": { + "jsonrpc": "2.0", + "result": { + "methods": [ + "account_nextIndex", + "archive_unstable_body", + "archive_unstable_call", + "archive_unstable_finalizedHeight", + "archive_unstable_genesisHash", + "archive_unstable_hashByHeight", + "archive_unstable_header", + "archive_unstable_storage", + "author_hasKey", + "author_hasSessionKeys", + "author_insertKey", + "author_pendingExtrinsics", + "author_removeExtrinsic", + "author_rotateKeys", + "author_submitAndWatchExtrinsic", + "author_submitExtrinsic", + "author_unwatchExtrinsic", + "chainHead_v1_body", + "chainHead_v1_call", + "chainHead_v1_continue", + "chainHead_v1_follow", + "chainHead_v1_header", + "chainHead_v1_stopOperation", + "chainHead_v1_storage", + "chainHead_v1_unfollow", + "chainHead_v1_unpin", + "chainSpec_v1_chainName", + "chainSpec_v1_genesisHash", + "chainSpec_v1_properties", + "chain_getBlock", + "chain_getBlockHash", + "chain_getFinalisedHead", + "chain_getFinalizedHead", + "chain_getHead", + "chain_getHeader", + "chain_getRuntimeVersion", + "chain_subscribeAllHeads", + "chain_subscribeFinalisedHeads", + "chain_subscribeFinalizedHeads", + "chain_subscribeNewHead", + "chain_subscribeNewHeads", + "chain_subscribeRuntimeVersion", + "chain_unsubscribeAllHeads", + "chain_unsubscribeFinalisedHeads", + "chain_unsubscribeFinalizedHeads", + "chain_unsubscribeNewHead", + "chain_unsubscribeNewHeads", + "chain_unsubscribeRuntimeVersion", + "childstate_getKeys", + "childstate_getKeysPaged", + "childstate_getKeysPagedAt", + "childstate_getStorage", + "childstate_getStorageEntries", + "childstate_getStorageHash", + "childstate_getStorageSize", + "debug_getBadBlocks", + "debug_getRawBlock", + "debug_getRawHeader", + "debug_getRawReceipts", + "debug_getRawTransaction", + "delegateInfo_getDelegate", + "delegateInfo_getDelegated", + "delegateInfo_getDelegates", + "eth_accounts", + "eth_blockNumber", + "eth_call", + "eth_chainId", + "eth_coinbase", + "eth_estimateGas", + "eth_feeHistory", + "eth_gasPrice", + "eth_getBalance", + "eth_getBlockByHash", + "eth_getBlockByNumber", + "eth_getBlockReceipts", + "eth_getBlockTransactionCountByHash", + "eth_getBlockTransactionCountByNumber", + "eth_getCode", + "eth_getFilterChanges", + "eth_getFilterLogs", + "eth_getLogs", + "eth_getStorageAt", + "eth_getTransactionByBlockHashAndIndex", + "eth_getTransactionByBlockNumberAndIndex", + "eth_getTransactionByHash", + "eth_getTransactionCount", + "eth_getTransactionReceipt", + "eth_getUncleByBlockHashAndIndex", + "eth_getUncleByBlockNumberAndIndex", + "eth_getUncleCountByBlockHash", + "eth_getUncleCountByBlockNumber", + "eth_getWork", + "eth_hashrate", + "eth_maxPriorityFeePerGas", + "eth_mining", + "eth_newBlockFilter", + "eth_newFilter", + "eth_newPendingTransactionFilter", + "eth_protocolVersion", + "eth_sendRawTransaction", + "eth_sendTransaction", + "eth_submitHashrate", + "eth_submitWork", + "eth_subscribe", + "eth_syncing", + "eth_uninstallFilter", + "eth_unsubscribe", + "net_listening", + "net_peerCount", + "net_version", + "neuronInfo_getNeuron", + "neuronInfo_getNeuronLite", + "neuronInfo_getNeurons", + "neuronInfo_getNeuronsLite", + "offchain_localStorageGet", + "offchain_localStorageSet", + "payment_queryFeeDetails", + "payment_queryInfo", + "rpc_methods", + "state_call", + "state_callAt", + "state_getChildReadProof", + "state_getKeys", + "state_getKeysPaged", + "state_getKeysPagedAt", + "state_getMetadata", + "state_getPairs", + "state_getReadProof", + "state_getRuntimeVersion", + "state_getStorage", + "state_getStorageAt", + "state_getStorageHash", + "state_getStorageHashAt", + "state_getStorageSize", + "state_getStorageSizeAt", + "state_queryStorage", + "state_queryStorageAt", + "state_subscribeRuntimeVersion", + "state_subscribeStorage", + "state_traceBlock", + "state_unsubscribeRuntimeVersion", + "state_unsubscribeStorage", + "subnetInfo_getLockCost", + "subnetInfo_getSubnetHyperparams", + "subnetInfo_getSubnetInfo", + "subnetInfo_getSubnetInfo_v2", + "subnetInfo_getSubnetsInf_v2", + "subnetInfo_getSubnetsInfo", + "subscribe_newHead", + "system_accountNextIndex", + "system_addLogFilter", + "system_addReservedPeer", + "system_chain", + "system_chainType", + "system_dryRun", + "system_dryRunAt", + "system_health", + "system_localListenAddresses", + "system_localPeerId", + "system_name", + "system_nodeRoles", + "system_peers", + "system_properties", + "system_removeReservedPeer", + "system_reservedPeers", + "system_resetLogFilter", + "system_syncState", + "system_unstable_networkState", + "system_version", + "transactionWatch_v1_submitAndWatch", + "transactionWatch_v1_unwatch", + "transaction_v1_broadcast", + "transaction_v1_stop", + "unsubscribe_newHead", + "web3_clientVersion", + "web3_sha3", + ] + }, + } + }, + "state_getKeysPaged": { + '["0x658faa385070e074c85bf6b568cf0555ea6aa4e81a33d2c120ef55203e0eb5603e9947bb89c47cd7ad643c5d30c0e6954af7598952f87683204a2a0c2266f941a74899bd69b0ce56e42a82820f689446", 100, "0x658faa385070e074c85bf6b568cf0555ea6aa4e81a33d2c120ef55203e0eb5603e9947bb89c47cd7ad643c5d30c0e6954af7598952f87683204a2a0c2266f941a74899bd69b0ce56e42a82820f689446", null]': { + "jsonrpc": "2.0", + "result": [ + "0x658faa385070e074c85bf6b568cf0555ea6aa4e81a33d2c120ef55203e0eb5603e9947bb89c47cd7ad643c5d30c0e6954af7598952f87683204a2a0c2266f941a74899bd69b0ce56e42a82820f6894461700" + ], + }, + '["0x658faa385070e074c85bf6b568cf0555ea6aa4e81a33d2c120ef55203e0eb5603e9947bb89c47cd7ad643c5d30c0e6954af7598952f87683204a2a0c2266f941a74899bd69b0ce56e42a82820f689446", 100, "0x658faa385070e074c85bf6b568cf0555ea6aa4e81a33d2c120ef55203e0eb5603e9947bb89c47cd7ad643c5d30c0e6954af7598952f87683204a2a0c2266f941a74899bd69b0ce56e42a82820f6894461700", null]': { + "jsonrpc": "2.0", + "result": [], + }, + }, + "state_getRuntimeVersion": { + '["0xdec7c1cfe134265d3bcb2b5c5b35ba6a08eedf29717fbce446d99e940bff72b5"]': { + "jsonrpc": "2.0", + "result": { + "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", 5], + ["0xe65b00e46cedd0aa", 2], + ["0x42e62be4a39e5b60", 1], + ["0x806df4ccaa9ed485", 1], + ["0x8375104b299b74c5", 1], + ["0x5d1fbfbe852f2807", 1], + ["0xc6886e2f8e598b0a", 1], + ], + "authoringVersion": 1, + "implName": "node-subtensor", + "implVersion": 1, + "specName": "node-subtensor", + "specVersion": 208, + "stateVersion": 1, + "transactionVersion": 1, + }, + } + }, + "state_queryStorageAt": { + '[["0x658faa385070e074c85bf6b568cf0555ea6aa4e81a33d2c120ef55203e0eb5603e9947bb89c47cd7ad643c5d30c0e6954af7598952f87683204a2a0c2266f941a74899bd69b0ce56e42a82820f6894461700"], null]': { + "jsonrpc": "2.0", + "result": [ + { + "block": "0x119e135f8ae6eac1f3fc47fc14838afcfdbee13e8dba14657ae150f4d56f6df2", + "changes": [ + [ + "0x658faa385070e074c85bf6b568cf0555ea6aa4e81a33d2c120ef55203e0eb5603e9947bb89c47cd7ad643c5d30c0e6954af7598952f87683204a2a0c2266f941a74899bd69b0ce56e42a82820f6894461700", + "0x01", + ] + ], + } + ], + } + }, + "system_chain": {"[]": {"jsonrpc": "2.0", "result": "Bittensor"}}, + }, + "is_hotkey_registered_any": { + "chain_getHead": { + "[]": { + "jsonrpc": "2.0", + "result": "0xdec7c1cfe134265d3bcb2b5c5b35ba6a08eedf29717fbce446d99e940bff72b5", + } + }, + "chain_getHeader": { + '["0xdec7c1cfe134265d3bcb2b5c5b35ba6a08eedf29717fbce446d99e940bff72b5"]': { + "jsonrpc": "2.0", + "result": { + "digest": { + "logs": [ + "0x0661757261209d489a0800000000", + "0x0466726f6e8801f1b41ac4894f8156a3e9314f37b91ec7ea6eef6c166ad10bdfc8b78286a4567e00", + "0x05617572610101d60d776e0fdf5277dcbd9acd297e3c0d8b6cae1e81b64091bb83082584258f433d4284eedfb31baf8d2144ead484c54c288fc8af21459aa78598e0a37e42148e", + ] + }, + "extrinsicsRoot": "0x1af6150accd523581af4c0ceafe50b19008f865d736364e221d5da78546f6259", + "number": "0x31ce90", + "parentHash": "0x1e326794f8c851e3b5675e603bf726120b074348a6c0d3e8ea354f6880a49f7b", + "stateRoot": "0x60dac4ae98a4ce08f9f8a416038ca0f4c359f7829d4cc064ad24ed31cfc7962a", + }, + } + }, + "rpc_methods": { + "[]": { + "jsonrpc": "2.0", + "result": { + "methods": [ + "account_nextIndex", + "archive_unstable_body", + "archive_unstable_call", + "archive_unstable_finalizedHeight", + "archive_unstable_genesisHash", + "archive_unstable_hashByHeight", + "archive_unstable_header", + "archive_unstable_storage", + "author_hasKey", + "author_hasSessionKeys", + "author_insertKey", + "author_pendingExtrinsics", + "author_removeExtrinsic", + "author_rotateKeys", + "author_submitAndWatchExtrinsic", + "author_submitExtrinsic", + "author_unwatchExtrinsic", + "chainHead_v1_body", + "chainHead_v1_call", + "chainHead_v1_continue", + "chainHead_v1_follow", + "chainHead_v1_header", + "chainHead_v1_stopOperation", + "chainHead_v1_storage", + "chainHead_v1_unfollow", + "chainHead_v1_unpin", + "chainSpec_v1_chainName", + "chainSpec_v1_genesisHash", + "chainSpec_v1_properties", + "chain_getBlock", + "chain_getBlockHash", + "chain_getFinalisedHead", + "chain_getFinalizedHead", + "chain_getHead", + "chain_getHeader", + "chain_getRuntimeVersion", + "chain_subscribeAllHeads", + "chain_subscribeFinalisedHeads", + "chain_subscribeFinalizedHeads", + "chain_subscribeNewHead", + "chain_subscribeNewHeads", + "chain_subscribeRuntimeVersion", + "chain_unsubscribeAllHeads", + "chain_unsubscribeFinalisedHeads", + "chain_unsubscribeFinalizedHeads", + "chain_unsubscribeNewHead", + "chain_unsubscribeNewHeads", + "chain_unsubscribeRuntimeVersion", + "childstate_getKeys", + "childstate_getKeysPaged", + "childstate_getKeysPagedAt", + "childstate_getStorage", + "childstate_getStorageEntries", + "childstate_getStorageHash", + "childstate_getStorageSize", + "debug_getBadBlocks", + "debug_getRawBlock", + "debug_getRawHeader", + "debug_getRawReceipts", + "debug_getRawTransaction", + "delegateInfo_getDelegate", + "delegateInfo_getDelegated", + "delegateInfo_getDelegates", + "eth_accounts", + "eth_blockNumber", + "eth_call", + "eth_chainId", + "eth_coinbase", + "eth_estimateGas", + "eth_feeHistory", + "eth_gasPrice", + "eth_getBalance", + "eth_getBlockByHash", + "eth_getBlockByNumber", + "eth_getBlockReceipts", + "eth_getBlockTransactionCountByHash", + "eth_getBlockTransactionCountByNumber", + "eth_getCode", + "eth_getFilterChanges", + "eth_getFilterLogs", + "eth_getLogs", + "eth_getStorageAt", + "eth_getTransactionByBlockHashAndIndex", + "eth_getTransactionByBlockNumberAndIndex", + "eth_getTransactionByHash", + "eth_getTransactionCount", + "eth_getTransactionReceipt", + "eth_getUncleByBlockHashAndIndex", + "eth_getUncleByBlockNumberAndIndex", + "eth_getUncleCountByBlockHash", + "eth_getUncleCountByBlockNumber", + "eth_getWork", + "eth_hashrate", + "eth_maxPriorityFeePerGas", + "eth_mining", + "eth_newBlockFilter", + "eth_newFilter", + "eth_newPendingTransactionFilter", + "eth_protocolVersion", + "eth_sendRawTransaction", + "eth_sendTransaction", + "eth_submitHashrate", + "eth_submitWork", + "eth_subscribe", + "eth_syncing", + "eth_uninstallFilter", + "eth_unsubscribe", + "net_listening", + "net_peerCount", + "net_version", + "neuronInfo_getNeuron", + "neuronInfo_getNeuronLite", + "neuronInfo_getNeurons", + "neuronInfo_getNeuronsLite", + "offchain_localStorageGet", + "offchain_localStorageSet", + "payment_queryFeeDetails", + "payment_queryInfo", + "rpc_methods", + "state_call", + "state_callAt", + "state_getChildReadProof", + "state_getKeys", + "state_getKeysPaged", + "state_getKeysPagedAt", + "state_getMetadata", + "state_getPairs", + "state_getReadProof", + "state_getRuntimeVersion", + "state_getStorage", + "state_getStorageAt", + "state_getStorageHash", + "state_getStorageHashAt", + "state_getStorageSize", + "state_getStorageSizeAt", + "state_queryStorage", + "state_queryStorageAt", + "state_subscribeRuntimeVersion", + "state_subscribeStorage", + "state_traceBlock", + "state_unsubscribeRuntimeVersion", + "state_unsubscribeStorage", + "subnetInfo_getLockCost", + "subnetInfo_getSubnetHyperparams", + "subnetInfo_getSubnetInfo", + "subnetInfo_getSubnetInfo_v2", + "subnetInfo_getSubnetsInf_v2", + "subnetInfo_getSubnetsInfo", + "subscribe_newHead", + "system_accountNextIndex", + "system_addLogFilter", + "system_addReservedPeer", + "system_chain", + "system_chainType", + "system_dryRun", + "system_dryRunAt", + "system_health", + "system_localListenAddresses", + "system_localPeerId", + "system_name", + "system_nodeRoles", + "system_peers", + "system_properties", + "system_removeReservedPeer", + "system_reservedPeers", + "system_resetLogFilter", + "system_syncState", + "system_unstable_networkState", + "system_version", + "transactionWatch_v1_submitAndWatch", + "transactionWatch_v1_unwatch", + "transaction_v1_broadcast", + "transaction_v1_stop", + "unsubscribe_newHead", + "web3_clientVersion", + "web3_sha3", + ] + }, + } + }, + "state_getKeysPaged": { + '["0x658faa385070e074c85bf6b568cf0555ea6aa4e81a33d2c120ef55203e0eb5603e9947bb89c47cd7ad643c5d30c0e6954af7598952f87683204a2a0c2266f941a74899bd69b0ce56e42a82820f689446", 100, "0x658faa385070e074c85bf6b568cf0555ea6aa4e81a33d2c120ef55203e0eb5603e9947bb89c47cd7ad643c5d30c0e6954af7598952f87683204a2a0c2266f941a74899bd69b0ce56e42a82820f689446", null]': { + "jsonrpc": "2.0", + "result": [ + "0x658faa385070e074c85bf6b568cf0555ea6aa4e81a33d2c120ef55203e0eb5603e9947bb89c47cd7ad643c5d30c0e6954af7598952f87683204a2a0c2266f941a74899bd69b0ce56e42a82820f6894461700" + ], + }, + '["0x658faa385070e074c85bf6b568cf0555ea6aa4e81a33d2c120ef55203e0eb5603e9947bb89c47cd7ad643c5d30c0e6954af7598952f87683204a2a0c2266f941a74899bd69b0ce56e42a82820f689446", 100, "0x658faa385070e074c85bf6b568cf0555ea6aa4e81a33d2c120ef55203e0eb5603e9947bb89c47cd7ad643c5d30c0e6954af7598952f87683204a2a0c2266f941a74899bd69b0ce56e42a82820f6894461700", null]': { + "jsonrpc": "2.0", + "result": [], + }, + }, + "state_getRuntimeVersion": { + '["0x1e326794f8c851e3b5675e603bf726120b074348a6c0d3e8ea354f6880a49f7b"]': { + "jsonrpc": "2.0", + "result": { + "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", 5], + ["0xe65b00e46cedd0aa", 2], + ["0x42e62be4a39e5b60", 1], + ["0x806df4ccaa9ed485", 1], + ["0x8375104b299b74c5", 1], + ["0x5d1fbfbe852f2807", 1], + ["0xc6886e2f8e598b0a", 1], + ], + "authoringVersion": 1, + "implName": "node-subtensor", + "implVersion": 1, + "specName": "node-subtensor", + "specVersion": 208, + "stateVersion": 1, + "transactionVersion": 1, + }, + } + }, + "state_queryStorageAt": { + '[["0x658faa385070e074c85bf6b568cf0555ea6aa4e81a33d2c120ef55203e0eb5603e9947bb89c47cd7ad643c5d30c0e6954af7598952f87683204a2a0c2266f941a74899bd69b0ce56e42a82820f6894461700"], null]': { + "jsonrpc": "2.0", + "result": [ + { + "block": "0xdec7c1cfe134265d3bcb2b5c5b35ba6a08eedf29717fbce446d99e940bff72b5", + "changes": [ + [ + "0x658faa385070e074c85bf6b568cf0555ea6aa4e81a33d2c120ef55203e0eb5603e9947bb89c47cd7ad643c5d30c0e6954af7598952f87683204a2a0c2266f941a74899bd69b0ce56e42a82820f6894461700", + "0x01", + ] + ], + } + ], + } + }, + "system_chain": {"[]": {"jsonrpc": "2.0", "result": "Bittensor"}}, + }, + "is_hotkey_registered_on_subnet": { + "chain_getHead": { + "[]": { + "jsonrpc": "2.0", + "result": "0xdec7c1cfe134265d3bcb2b5c5b35ba6a08eedf29717fbce446d99e940bff72b5", + } + }, + "chain_getHeader": { + '["0xdec7c1cfe134265d3bcb2b5c5b35ba6a08eedf29717fbce446d99e940bff72b5"]': { + "jsonrpc": "2.0", + "result": { + "digest": { + "logs": [ + "0x0661757261209d489a0800000000", + "0x0466726f6e8801f1b41ac4894f8156a3e9314f37b91ec7ea6eef6c166ad10bdfc8b78286a4567e00", + "0x05617572610101d60d776e0fdf5277dcbd9acd297e3c0d8b6cae1e81b64091bb83082584258f433d4284eedfb31baf8d2144ead484c54c288fc8af21459aa78598e0a37e42148e", + ] + }, + "extrinsicsRoot": "0x1af6150accd523581af4c0ceafe50b19008f865d736364e221d5da78546f6259", + "number": "0x31ce90", + "parentHash": "0x1e326794f8c851e3b5675e603bf726120b074348a6c0d3e8ea354f6880a49f7b", + "stateRoot": "0x60dac4ae98a4ce08f9f8a416038ca0f4c359f7829d4cc064ad24ed31cfc7962a", + }, + } + }, + "rpc_methods": { + "[]": { + "jsonrpc": "2.0", + "result": { + "methods": [ + "account_nextIndex", + "archive_unstable_body", + "archive_unstable_call", + "archive_unstable_finalizedHeight", + "archive_unstable_genesisHash", + "archive_unstable_hashByHeight", + "archive_unstable_header", + "archive_unstable_storage", + "author_hasKey", + "author_hasSessionKeys", + "author_insertKey", + "author_pendingExtrinsics", + "author_removeExtrinsic", + "author_rotateKeys", + "author_submitAndWatchExtrinsic", + "author_submitExtrinsic", + "author_unwatchExtrinsic", + "chainHead_v1_body", + "chainHead_v1_call", + "chainHead_v1_continue", + "chainHead_v1_follow", + "chainHead_v1_header", + "chainHead_v1_stopOperation", + "chainHead_v1_storage", + "chainHead_v1_unfollow", + "chainHead_v1_unpin", + "chainSpec_v1_chainName", + "chainSpec_v1_genesisHash", + "chainSpec_v1_properties", + "chain_getBlock", + "chain_getBlockHash", + "chain_getFinalisedHead", + "chain_getFinalizedHead", + "chain_getHead", + "chain_getHeader", + "chain_getRuntimeVersion", + "chain_subscribeAllHeads", + "chain_subscribeFinalisedHeads", + "chain_subscribeFinalizedHeads", + "chain_subscribeNewHead", + "chain_subscribeNewHeads", + "chain_subscribeRuntimeVersion", + "chain_unsubscribeAllHeads", + "chain_unsubscribeFinalisedHeads", + "chain_unsubscribeFinalizedHeads", + "chain_unsubscribeNewHead", + "chain_unsubscribeNewHeads", + "chain_unsubscribeRuntimeVersion", + "childstate_getKeys", + "childstate_getKeysPaged", + "childstate_getKeysPagedAt", + "childstate_getStorage", + "childstate_getStorageEntries", + "childstate_getStorageHash", + "childstate_getStorageSize", + "debug_getBadBlocks", + "debug_getRawBlock", + "debug_getRawHeader", + "debug_getRawReceipts", + "debug_getRawTransaction", + "delegateInfo_getDelegate", + "delegateInfo_getDelegated", + "delegateInfo_getDelegates", + "eth_accounts", + "eth_blockNumber", + "eth_call", + "eth_chainId", + "eth_coinbase", + "eth_estimateGas", + "eth_feeHistory", + "eth_gasPrice", + "eth_getBalance", + "eth_getBlockByHash", + "eth_getBlockByNumber", + "eth_getBlockReceipts", + "eth_getBlockTransactionCountByHash", + "eth_getBlockTransactionCountByNumber", + "eth_getCode", + "eth_getFilterChanges", + "eth_getFilterLogs", + "eth_getLogs", + "eth_getStorageAt", + "eth_getTransactionByBlockHashAndIndex", + "eth_getTransactionByBlockNumberAndIndex", + "eth_getTransactionByHash", + "eth_getTransactionCount", + "eth_getTransactionReceipt", + "eth_getUncleByBlockHashAndIndex", + "eth_getUncleByBlockNumberAndIndex", + "eth_getUncleCountByBlockHash", + "eth_getUncleCountByBlockNumber", + "eth_getWork", + "eth_hashrate", + "eth_maxPriorityFeePerGas", + "eth_mining", + "eth_newBlockFilter", + "eth_newFilter", + "eth_newPendingTransactionFilter", + "eth_protocolVersion", + "eth_sendRawTransaction", + "eth_sendTransaction", + "eth_submitHashrate", + "eth_submitWork", + "eth_subscribe", + "eth_syncing", + "eth_uninstallFilter", + "eth_unsubscribe", + "net_listening", + "net_peerCount", + "net_version", + "neuronInfo_getNeuron", + "neuronInfo_getNeuronLite", + "neuronInfo_getNeurons", + "neuronInfo_getNeuronsLite", + "offchain_localStorageGet", + "offchain_localStorageSet", + "payment_queryFeeDetails", + "payment_queryInfo", + "rpc_methods", + "state_call", + "state_callAt", + "state_getChildReadProof", + "state_getKeys", + "state_getKeysPaged", + "state_getKeysPagedAt", + "state_getMetadata", + "state_getPairs", + "state_getReadProof", + "state_getRuntimeVersion", + "state_getStorage", + "state_getStorageAt", + "state_getStorageHash", + "state_getStorageHashAt", + "state_getStorageSize", + "state_getStorageSizeAt", + "state_queryStorage", + "state_queryStorageAt", + "state_subscribeRuntimeVersion", + "state_subscribeStorage", + "state_traceBlock", + "state_unsubscribeRuntimeVersion", + "state_unsubscribeStorage", + "subnetInfo_getLockCost", + "subnetInfo_getSubnetHyperparams", + "subnetInfo_getSubnetInfo", + "subnetInfo_getSubnetInfo_v2", + "subnetInfo_getSubnetsInf_v2", + "subnetInfo_getSubnetsInfo", + "subscribe_newHead", + "system_accountNextIndex", + "system_addLogFilter", + "system_addReservedPeer", + "system_chain", + "system_chainType", + "system_dryRun", + "system_dryRunAt", + "system_health", + "system_localListenAddresses", + "system_localPeerId", + "system_name", + "system_nodeRoles", + "system_peers", + "system_properties", + "system_removeReservedPeer", + "system_reservedPeers", + "system_resetLogFilter", + "system_syncState", + "system_unstable_networkState", + "system_version", + "transactionWatch_v1_submitAndWatch", + "transactionWatch_v1_unwatch", + "transaction_v1_broadcast", + "transaction_v1_stop", + "unsubscribe_newHead", + "web3_clientVersion", + "web3_sha3", + ] + }, + } + }, + "state_getRuntimeVersion": { + '["0x1e326794f8c851e3b5675e603bf726120b074348a6c0d3e8ea354f6880a49f7b"]': { + "jsonrpc": "2.0", + "result": { + "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", 5], + ["0xe65b00e46cedd0aa", 2], + ["0x42e62be4a39e5b60", 1], + ["0x806df4ccaa9ed485", 1], + ["0x8375104b299b74c5", 1], + ["0x5d1fbfbe852f2807", 1], + ["0xc6886e2f8e598b0a", 1], + ], + "authoringVersion": 1, + "implName": "node-subtensor", + "implVersion": 1, + "specName": "node-subtensor", + "specVersion": 208, + "stateVersion": 1, + "transactionVersion": 1, + }, + } + }, + "state_getStorageAt": { + '["0x658faa385070e074c85bf6b568cf0555aab1b4e78e1ea8305462ee53b3686dc817003e9947bb89c47cd7ad643c5d30c0e6954af7598952f87683204a2a0c2266f941a74899bd69b0ce56e42a82820f689446", null]': { + "jsonrpc": "2.0", + "result": "0x0500", + } + }, + "system_chain": {"[]": {"jsonrpc": "2.0", "result": "Bittensor"}}, + }, + "max_weight_limit": { + "chain_getHead": { + "[]": { + "jsonrpc": "2.0", + "result": "0x056fb97cc61bb3892f5c1f58574cd8a489ccfb7681079137b7bb2f7aaaaeef35", + } + }, + "chain_getHeader": { + '["0x056fb97cc61bb3892f5c1f58574cd8a489ccfb7681079137b7bb2f7aaaaeef35"]': { + "jsonrpc": "2.0", + "result": { + "digest": { + "logs": [ + "0x066175726120a5489a0800000000", + "0x0466726f6e8801ccff6ee8bfd5cb83806ecf288817281fb0a712da379ecf363eadf1ab5b4b3ab000", + "0x05617572610101d079b4203eb95e04a29c4ecc3525d4084d5098630786fd90d876d9d9a6e03c0c1b66528cf6d847f935844060118a91b0a8a830006cb2667b507bc89bdad6c787", + ] + }, + "extrinsicsRoot": "0x19b29a10074df12d64e3b69536db01b02fb097da6c47ae0d7b387e789684a75d", + "number": "0x31ce98", + "parentHash": "0xa049374c707f9a426866a090c66eff7106a75597dd0d87a8fbe39a7f7887ed16", + "stateRoot": "0xfd8cc166992fad3e4740037bf9f4231853b460a2f1a173aa3e1cea426f05592f", + }, + } + }, + "rpc_methods": { + "[]": { + "jsonrpc": "2.0", + "result": { + "methods": [ + "account_nextIndex", + "archive_unstable_body", + "archive_unstable_call", + "archive_unstable_finalizedHeight", + "archive_unstable_genesisHash", + "archive_unstable_hashByHeight", + "archive_unstable_header", + "archive_unstable_storage", + "author_hasKey", + "author_hasSessionKeys", + "author_insertKey", + "author_pendingExtrinsics", + "author_removeExtrinsic", + "author_rotateKeys", + "author_submitAndWatchExtrinsic", + "author_submitExtrinsic", + "author_unwatchExtrinsic", + "chainHead_v1_body", + "chainHead_v1_call", + "chainHead_v1_continue", + "chainHead_v1_follow", + "chainHead_v1_header", + "chainHead_v1_stopOperation", + "chainHead_v1_storage", + "chainHead_v1_unfollow", + "chainHead_v1_unpin", + "chainSpec_v1_chainName", + "chainSpec_v1_genesisHash", + "chainSpec_v1_properties", + "chain_getBlock", + "chain_getBlockHash", + "chain_getFinalisedHead", + "chain_getFinalizedHead", + "chain_getHead", + "chain_getHeader", + "chain_getRuntimeVersion", + "chain_subscribeAllHeads", + "chain_subscribeFinalisedHeads", + "chain_subscribeFinalizedHeads", + "chain_subscribeNewHead", + "chain_subscribeNewHeads", + "chain_subscribeRuntimeVersion", + "chain_unsubscribeAllHeads", + "chain_unsubscribeFinalisedHeads", + "chain_unsubscribeFinalizedHeads", + "chain_unsubscribeNewHead", + "chain_unsubscribeNewHeads", + "chain_unsubscribeRuntimeVersion", + "childstate_getKeys", + "childstate_getKeysPaged", + "childstate_getKeysPagedAt", + "childstate_getStorage", + "childstate_getStorageEntries", + "childstate_getStorageHash", + "childstate_getStorageSize", + "debug_getBadBlocks", + "debug_getRawBlock", + "debug_getRawHeader", + "debug_getRawReceipts", + "debug_getRawTransaction", + "delegateInfo_getDelegate", + "delegateInfo_getDelegated", + "delegateInfo_getDelegates", + "eth_accounts", + "eth_blockNumber", + "eth_call", + "eth_chainId", + "eth_coinbase", + "eth_estimateGas", + "eth_feeHistory", + "eth_gasPrice", + "eth_getBalance", + "eth_getBlockByHash", + "eth_getBlockByNumber", + "eth_getBlockReceipts", + "eth_getBlockTransactionCountByHash", + "eth_getBlockTransactionCountByNumber", + "eth_getCode", + "eth_getFilterChanges", + "eth_getFilterLogs", + "eth_getLogs", + "eth_getStorageAt", + "eth_getTransactionByBlockHashAndIndex", + "eth_getTransactionByBlockNumberAndIndex", + "eth_getTransactionByHash", + "eth_getTransactionCount", + "eth_getTransactionReceipt", + "eth_getUncleByBlockHashAndIndex", + "eth_getUncleByBlockNumberAndIndex", + "eth_getUncleCountByBlockHash", + "eth_getUncleCountByBlockNumber", + "eth_getWork", + "eth_hashrate", + "eth_maxPriorityFeePerGas", + "eth_mining", + "eth_newBlockFilter", + "eth_newFilter", + "eth_newPendingTransactionFilter", + "eth_protocolVersion", + "eth_sendRawTransaction", + "eth_sendTransaction", + "eth_submitHashrate", + "eth_submitWork", + "eth_subscribe", + "eth_syncing", + "eth_uninstallFilter", + "eth_unsubscribe", + "net_listening", + "net_peerCount", + "net_version", + "neuronInfo_getNeuron", + "neuronInfo_getNeuronLite", + "neuronInfo_getNeurons", + "neuronInfo_getNeuronsLite", + "offchain_localStorageGet", + "offchain_localStorageSet", + "payment_queryFeeDetails", + "payment_queryInfo", + "rpc_methods", + "state_call", + "state_callAt", + "state_getChildReadProof", + "state_getKeys", + "state_getKeysPaged", + "state_getKeysPagedAt", + "state_getMetadata", + "state_getPairs", + "state_getReadProof", + "state_getRuntimeVersion", + "state_getStorage", + "state_getStorageAt", + "state_getStorageHash", + "state_getStorageHashAt", + "state_getStorageSize", + "state_getStorageSizeAt", + "state_queryStorage", + "state_queryStorageAt", + "state_subscribeRuntimeVersion", + "state_subscribeStorage", + "state_traceBlock", + "state_unsubscribeRuntimeVersion", + "state_unsubscribeStorage", + "subnetInfo_getLockCost", + "subnetInfo_getSubnetHyperparams", + "subnetInfo_getSubnetInfo", + "subnetInfo_getSubnetInfo_v2", + "subnetInfo_getSubnetsInf_v2", + "subnetInfo_getSubnetsInfo", + "subscribe_newHead", + "system_accountNextIndex", + "system_addLogFilter", + "system_addReservedPeer", + "system_chain", + "system_chainType", + "system_dryRun", + "system_dryRunAt", + "system_health", + "system_localListenAddresses", + "system_localPeerId", + "system_name", + "system_nodeRoles", + "system_peers", + "system_properties", + "system_removeReservedPeer", + "system_reservedPeers", + "system_resetLogFilter", + "system_syncState", + "system_unstable_networkState", + "system_version", + "transactionWatch_v1_submitAndWatch", + "transactionWatch_v1_unwatch", + "transaction_v1_broadcast", + "transaction_v1_stop", + "unsubscribe_newHead", + "web3_clientVersion", + "web3_sha3", + ] + }, + } + }, + "state_getRuntimeVersion": { + '["0xa049374c707f9a426866a090c66eff7106a75597dd0d87a8fbe39a7f7887ed16"]': { + "jsonrpc": "2.0", + "result": { + "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", 5], + ["0xe65b00e46cedd0aa", 2], + ["0x42e62be4a39e5b60", 1], + ["0x806df4ccaa9ed485", 1], + ["0x8375104b299b74c5", 1], + ["0x5d1fbfbe852f2807", 1], + ["0xc6886e2f8e598b0a", 1], + ], + "authoringVersion": 1, + "implName": "node-subtensor", + "implVersion": 1, + "specName": "node-subtensor", + "specVersion": 208, + "stateVersion": 1, + "transactionVersion": 1, + }, + } + }, + "state_getStorageAt": { + '["0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a431700", "0x056fb97cc61bb3892f5c1f58574cd8a489ccfb7681079137b7bb2f7aaaaeef35"]': { + "jsonrpc": "2.0", + "result": "0x01", + }, + '["0x658faa385070e074c85bf6b568cf0555919db2fe18203eba898cee471ef192401700", "0x056fb97cc61bb3892f5c1f58574cd8a489ccfb7681079137b7bb2f7aaaaeef35"]': { + "jsonrpc": "2.0", + "result": "0xffff", + }, + }, + "system_chain": {"[]": {"jsonrpc": "2.0", "result": "Bittensor"}}, + }, + "metagraph": { + "chain_getHead": { + "[]": { + "jsonrpc": "2.0", + "result": "0xe9729c54c0e59c611198b560a7a93e52154100187d04dfc09d1dc1a6572d4006", + } + }, + "chain_getHeader": { + '["0xe9729c54c0e59c611198b560a7a93e52154100187d04dfc09d1dc1a6572d4006"]': { + "jsonrpc": "2.0", + "result": { + "digest": { + "logs": [ + "0x0661757261209b489a0800000000", + "0x0466726f6e8801fa9bcb7befe1394a353134de30ad705326185473f91cd6be31397c06759e007800", + "0x0561757261010128b382541b4a78e5c9ff3cee4ae48fe3c5337add9f1baad15b72939990218773794b348babefd9fa28312c5b5097b52b034d90a331a747dcfcd03fa37a7fb482", + ] + }, + "extrinsicsRoot": "0xfabdc25842a79c97f6029f17f0f7521ec5f9b41815932f1dcc2cb0752fa0ca9a", + "number": "0x31ce8e", + "parentHash": "0xae0ef35761d050ada6d4f09efec39ca430d4f4c50b927741b32fd487d382ead8", + "stateRoot": "0x7da2cf163b71981ea914cc9f8ddecf662faee1f6f04acf5e814a16f086ddbe78", + }, + }, + "[null]": { + "jsonrpc": "2.0", + "result": { + "digest": { + "logs": [ + "0x0661757261209c489a0800000000", + "0x0466726f6e8801b81937c0aed82aace40c1860c8f8be871ed90466eb702dcd50fef49a70ca8dcf00", + "0x056175726101016a8bbee0a2b31058eff0df90b3b194cc7824e735e09b957136291639ff36c2047102e48742ac2ac14fe1634b652bba055b00383b06cea6482c56755b74c3c88b", + ] + }, + "extrinsicsRoot": "0x42acc4ffcaba39f003a14f13e2dc69e5b93198724a019515f31e22baf0b240f7", + "number": "0x31ce8f", + "parentHash": "0xe9729c54c0e59c611198b560a7a93e52154100187d04dfc09d1dc1a6572d4006", + "stateRoot": "0x319de5cb67bbf31e93a7db2e75b9bca48ce8d0b91d5156ce2c738eb178700d98", + }, + }, + }, + "rpc_methods": { + "[]": { + "jsonrpc": "2.0", + "result": { + "methods": [ + "account_nextIndex", + "archive_unstable_body", + "archive_unstable_call", + "archive_unstable_finalizedHeight", + "archive_unstable_genesisHash", + "archive_unstable_hashByHeight", + "archive_unstable_header", + "archive_unstable_storage", + "author_hasKey", + "author_hasSessionKeys", + "author_insertKey", + "author_pendingExtrinsics", + "author_removeExtrinsic", + "author_rotateKeys", + "author_submitAndWatchExtrinsic", + "author_submitExtrinsic", + "author_unwatchExtrinsic", + "chainHead_v1_body", + "chainHead_v1_call", + "chainHead_v1_continue", + "chainHead_v1_follow", + "chainHead_v1_header", + "chainHead_v1_stopOperation", + "chainHead_v1_storage", + "chainHead_v1_unfollow", + "chainHead_v1_unpin", + "chainSpec_v1_chainName", + "chainSpec_v1_genesisHash", + "chainSpec_v1_properties", + "chain_getBlock", + "chain_getBlockHash", + "chain_getFinalisedHead", + "chain_getFinalizedHead", + "chain_getHead", + "chain_getHeader", + "chain_getRuntimeVersion", + "chain_subscribeAllHeads", + "chain_subscribeFinalisedHeads", + "chain_subscribeFinalizedHeads", + "chain_subscribeNewHead", + "chain_subscribeNewHeads", + "chain_subscribeRuntimeVersion", + "chain_unsubscribeAllHeads", + "chain_unsubscribeFinalisedHeads", + "chain_unsubscribeFinalizedHeads", + "chain_unsubscribeNewHead", + "chain_unsubscribeNewHeads", + "chain_unsubscribeRuntimeVersion", + "childstate_getKeys", + "childstate_getKeysPaged", + "childstate_getKeysPagedAt", + "childstate_getStorage", + "childstate_getStorageEntries", + "childstate_getStorageHash", + "childstate_getStorageSize", + "debug_getBadBlocks", + "debug_getRawBlock", + "debug_getRawHeader", + "debug_getRawReceipts", + "debug_getRawTransaction", + "delegateInfo_getDelegate", + "delegateInfo_getDelegated", + "delegateInfo_getDelegates", + "eth_accounts", + "eth_blockNumber", + "eth_call", + "eth_chainId", + "eth_coinbase", + "eth_estimateGas", + "eth_feeHistory", + "eth_gasPrice", + "eth_getBalance", + "eth_getBlockByHash", + "eth_getBlockByNumber", + "eth_getBlockReceipts", + "eth_getBlockTransactionCountByHash", + "eth_getBlockTransactionCountByNumber", + "eth_getCode", + "eth_getFilterChanges", + "eth_getFilterLogs", + "eth_getLogs", + "eth_getStorageAt", + "eth_getTransactionByBlockHashAndIndex", + "eth_getTransactionByBlockNumberAndIndex", + "eth_getTransactionByHash", + "eth_getTransactionCount", + "eth_getTransactionReceipt", + "eth_getUncleByBlockHashAndIndex", + "eth_getUncleByBlockNumberAndIndex", + "eth_getUncleCountByBlockHash", + "eth_getUncleCountByBlockNumber", + "eth_getWork", + "eth_hashrate", + "eth_maxPriorityFeePerGas", + "eth_mining", + "eth_newBlockFilter", + "eth_newFilter", + "eth_newPendingTransactionFilter", + "eth_protocolVersion", + "eth_sendRawTransaction", + "eth_sendTransaction", + "eth_submitHashrate", + "eth_submitWork", + "eth_subscribe", + "eth_syncing", + "eth_uninstallFilter", + "eth_unsubscribe", + "net_listening", + "net_peerCount", + "net_version", + "neuronInfo_getNeuron", + "neuronInfo_getNeuronLite", + "neuronInfo_getNeurons", + "neuronInfo_getNeuronsLite", + "offchain_localStorageGet", + "offchain_localStorageSet", + "payment_queryFeeDetails", + "payment_queryInfo", + "rpc_methods", + "state_call", + "state_callAt", + "state_getChildReadProof", + "state_getKeys", + "state_getKeysPaged", + "state_getKeysPagedAt", + "state_getMetadata", + "state_getPairs", + "state_getReadProof", + "state_getRuntimeVersion", + "state_getStorage", + "state_getStorageAt", + "state_getStorageHash", + "state_getStorageHashAt", + "state_getStorageSize", + "state_getStorageSizeAt", + "state_queryStorage", + "state_queryStorageAt", + "state_subscribeRuntimeVersion", + "state_subscribeStorage", + "state_traceBlock", + "state_unsubscribeRuntimeVersion", + "state_unsubscribeStorage", + "subnetInfo_getLockCost", + "subnetInfo_getSubnetHyperparams", + "subnetInfo_getSubnetInfo", + "subnetInfo_getSubnetInfo_v2", + "subnetInfo_getSubnetsInf_v2", + "subnetInfo_getSubnetsInfo", + "subscribe_newHead", + "system_accountNextIndex", + "system_addLogFilter", + "system_addReservedPeer", + "system_chain", + "system_chainType", + "system_dryRun", + "system_dryRunAt", + "system_health", + "system_localListenAddresses", + "system_localPeerId", + "system_name", + "system_nodeRoles", + "system_peers", + "system_properties", + "system_removeReservedPeer", + "system_reservedPeers", + "system_resetLogFilter", + "system_syncState", + "system_unstable_networkState", + "system_version", + "transactionWatch_v1_submitAndWatch", + "transactionWatch_v1_unwatch", + "transaction_v1_broadcast", + "transaction_v1_stop", + "unsubscribe_newHead", + "web3_clientVersion", + "web3_sha3", + ] + }, + } + }, + "state_call": { + '["NeuronInfoRuntimeApi_get_neurons_lite", "0100", null]': { + "jsonrpc": "2.0", + "result": "0x040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000bb06f5e8aae0700072af77b481300000000000001feff0300", + }, + '["SubnetInfoRuntimeApi_get_subnet_state", "0100", null]': { + "jsonrpc": "2.0", + "result": "0x01040400000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000400040104feff0300040004072af77b4813040004000400040004000400040ba63a463d22080400040ba63a463d22080804000400", + }, + '["SubnetInfoRuntimeApi_get_metagraph", "0100", null]': { + "jsonrpc": "2.0", + "result": "0x00", + }, + }, + "state_getRuntimeVersion": { + '["0xae0ef35761d050ada6d4f09efec39ca430d4f4c50b927741b32fd487d382ead8"]': { + "jsonrpc": "2.0", + "result": { + "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", 5], + ["0xe65b00e46cedd0aa", 2], + ["0x42e62be4a39e5b60", 1], + ["0x806df4ccaa9ed485", 1], + ["0x8375104b299b74c5", 1], + ["0x5d1fbfbe852f2807", 1], + ["0xc6886e2f8e598b0a", 1], + ], + "authoringVersion": 1, + "implName": "node-subtensor", + "implVersion": 1, + "specName": "node-subtensor", + "specVersion": 208, + "stateVersion": 1, + "transactionVersion": 1, + }, + } + }, + "system_chain": {"[]": {"jsonrpc": "2.0", "result": "Bittensor"}}, + "chain_getBlockHash": { + "[3264143]": { + "jsonrpc": "2.0", + "result": None, + } + }, + }, + "min_allowed_weights": { + "chain_getHead": { + "[]": { + "jsonrpc": "2.0", + "result": "0x056fb97cc61bb3892f5c1f58574cd8a489ccfb7681079137b7bb2f7aaaaeef35", + } + }, + "chain_getHeader": { + '["0x056fb97cc61bb3892f5c1f58574cd8a489ccfb7681079137b7bb2f7aaaaeef35"]': { + "jsonrpc": "2.0", + "result": { + "digest": { + "logs": [ + "0x066175726120a5489a0800000000", + "0x0466726f6e8801ccff6ee8bfd5cb83806ecf288817281fb0a712da379ecf363eadf1ab5b4b3ab000", + "0x05617572610101d079b4203eb95e04a29c4ecc3525d4084d5098630786fd90d876d9d9a6e03c0c1b66528cf6d847f935844060118a91b0a8a830006cb2667b507bc89bdad6c787", + ] + }, + "extrinsicsRoot": "0x19b29a10074df12d64e3b69536db01b02fb097da6c47ae0d7b387e789684a75d", + "number": "0x31ce98", + "parentHash": "0xa049374c707f9a426866a090c66eff7106a75597dd0d87a8fbe39a7f7887ed16", + "stateRoot": "0xfd8cc166992fad3e4740037bf9f4231853b460a2f1a173aa3e1cea426f05592f", + }, + }, + '["0xa049374c707f9a426866a090c66eff7106a75597dd0d87a8fbe39a7f7887ed16"]': { + "jsonrpc": "2.0", + "result": { + "digest": { + "logs": [ + "0x066175726120a4489a0800000000", + "0x0466726f6e88010f9ddbd627a7831fa708913774b9c4dc6bd6b5e67c7bc202212ec123eafe0fea00", + "0x056175726101015e937529bffb8837c11a4784fa1638a519eada7bb9a98c484917a6ce84e92f24cafbe4a2e057813f52488d37ad9bd0402f03dac6ed62bc8f0f678bd3326bdc85", + ] + }, + "extrinsicsRoot": "0x3d4fcc97011a4f12d9c64074b37a39a440c9fdfb38cd4f4923be189c36a55fc2", + "number": "0x31ce97", + "parentHash": "0x03a008636efbe7706f79f2c7d7d4e85ff26a3e67922e5fefab71e7f6d9dab43d", + "stateRoot": "0x0f0943f73c54721a2e4ef101e7de798b8b8e4e72681703d694dde6df3d797d82", + }, + }, + }, + "rpc_methods": { + "[]": { + "jsonrpc": "2.0", + "result": { + "methods": [ + "account_nextIndex", + "archive_unstable_body", + "archive_unstable_call", + "archive_unstable_finalizedHeight", + "archive_unstable_genesisHash", + "archive_unstable_hashByHeight", + "archive_unstable_header", + "archive_unstable_storage", + "author_hasKey", + "author_hasSessionKeys", + "author_insertKey", + "author_pendingExtrinsics", + "author_removeExtrinsic", + "author_rotateKeys", + "author_submitAndWatchExtrinsic", + "author_submitExtrinsic", + "author_unwatchExtrinsic", + "chainHead_v1_body", + "chainHead_v1_call", + "chainHead_v1_continue", + "chainHead_v1_follow", + "chainHead_v1_header", + "chainHead_v1_stopOperation", + "chainHead_v1_storage", + "chainHead_v1_unfollow", + "chainHead_v1_unpin", + "chainSpec_v1_chainName", + "chainSpec_v1_genesisHash", + "chainSpec_v1_properties", + "chain_getBlock", + "chain_getBlockHash", + "chain_getFinalisedHead", + "chain_getFinalizedHead", + "chain_getHead", + "chain_getHeader", + "chain_getRuntimeVersion", + "chain_subscribeAllHeads", + "chain_subscribeFinalisedHeads", + "chain_subscribeFinalizedHeads", + "chain_subscribeNewHead", + "chain_subscribeNewHeads", + "chain_subscribeRuntimeVersion", + "chain_unsubscribeAllHeads", + "chain_unsubscribeFinalisedHeads", + "chain_unsubscribeFinalizedHeads", + "chain_unsubscribeNewHead", + "chain_unsubscribeNewHeads", + "chain_unsubscribeRuntimeVersion", + "childstate_getKeys", + "childstate_getKeysPaged", + "childstate_getKeysPagedAt", + "childstate_getStorage", + "childstate_getStorageEntries", + "childstate_getStorageHash", + "childstate_getStorageSize", + "debug_getBadBlocks", + "debug_getRawBlock", + "debug_getRawHeader", + "debug_getRawReceipts", + "debug_getRawTransaction", + "delegateInfo_getDelegate", + "delegateInfo_getDelegated", + "delegateInfo_getDelegates", + "eth_accounts", + "eth_blockNumber", + "eth_call", + "eth_chainId", + "eth_coinbase", + "eth_estimateGas", + "eth_feeHistory", + "eth_gasPrice", + "eth_getBalance", + "eth_getBlockByHash", + "eth_getBlockByNumber", + "eth_getBlockReceipts", + "eth_getBlockTransactionCountByHash", + "eth_getBlockTransactionCountByNumber", + "eth_getCode", + "eth_getFilterChanges", + "eth_getFilterLogs", + "eth_getLogs", + "eth_getStorageAt", + "eth_getTransactionByBlockHashAndIndex", + "eth_getTransactionByBlockNumberAndIndex", + "eth_getTransactionByHash", + "eth_getTransactionCount", + "eth_getTransactionReceipt", + "eth_getUncleByBlockHashAndIndex", + "eth_getUncleByBlockNumberAndIndex", + "eth_getUncleCountByBlockHash", + "eth_getUncleCountByBlockNumber", + "eth_getWork", + "eth_hashrate", + "eth_maxPriorityFeePerGas", + "eth_mining", + "eth_newBlockFilter", + "eth_newFilter", + "eth_newPendingTransactionFilter", + "eth_protocolVersion", + "eth_sendRawTransaction", + "eth_sendTransaction", + "eth_submitHashrate", + "eth_submitWork", + "eth_subscribe", + "eth_syncing", + "eth_uninstallFilter", + "eth_unsubscribe", + "net_listening", + "net_peerCount", + "net_version", + "neuronInfo_getNeuron", + "neuronInfo_getNeuronLite", + "neuronInfo_getNeurons", + "neuronInfo_getNeuronsLite", + "offchain_localStorageGet", + "offchain_localStorageSet", + "payment_queryFeeDetails", + "payment_queryInfo", + "rpc_methods", + "state_call", + "state_callAt", + "state_getChildReadProof", + "state_getKeys", + "state_getKeysPaged", + "state_getKeysPagedAt", + "state_getMetadata", + "state_getPairs", + "state_getReadProof", + "state_getRuntimeVersion", + "state_getStorage", + "state_getStorageAt", + "state_getStorageHash", + "state_getStorageHashAt", + "state_getStorageSize", + "state_getStorageSizeAt", + "state_queryStorage", + "state_queryStorageAt", + "state_subscribeRuntimeVersion", + "state_subscribeStorage", + "state_traceBlock", + "state_unsubscribeRuntimeVersion", + "state_unsubscribeStorage", + "subnetInfo_getLockCost", + "subnetInfo_getSubnetHyperparams", + "subnetInfo_getSubnetInfo", + "subnetInfo_getSubnetInfo_v2", + "subnetInfo_getSubnetsInf_v2", + "subnetInfo_getSubnetsInfo", + "subscribe_newHead", + "system_accountNextIndex", + "system_addLogFilter", + "system_addReservedPeer", + "system_chain", + "system_chainType", + "system_dryRun", + "system_dryRunAt", + "system_health", + "system_localListenAddresses", + "system_localPeerId", + "system_name", + "system_nodeRoles", + "system_peers", + "system_properties", + "system_removeReservedPeer", + "system_reservedPeers", + "system_resetLogFilter", + "system_syncState", + "system_unstable_networkState", + "system_version", + "transactionWatch_v1_submitAndWatch", + "transactionWatch_v1_unwatch", + "transaction_v1_broadcast", + "transaction_v1_stop", + "unsubscribe_newHead", + "web3_clientVersion", + "web3_sha3", + ] + }, + } + }, + "state_getRuntimeVersion": { + '["0x03a008636efbe7706f79f2c7d7d4e85ff26a3e67922e5fefab71e7f6d9dab43d"]': { + "jsonrpc": "2.0", + "result": { + "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", 5], + ["0xe65b00e46cedd0aa", 2], + ["0x42e62be4a39e5b60", 1], + ["0x806df4ccaa9ed485", 1], + ["0x8375104b299b74c5", 1], + ["0x5d1fbfbe852f2807", 1], + ["0xc6886e2f8e598b0a", 1], + ], + "authoringVersion": 1, + "implName": "node-subtensor", + "implVersion": 1, + "specName": "node-subtensor", + "specVersion": 208, + "stateVersion": 1, + "transactionVersion": 1, + }, + }, + '["0xa049374c707f9a426866a090c66eff7106a75597dd0d87a8fbe39a7f7887ed16"]': { + "jsonrpc": "2.0", + "result": { + "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", 5], + ["0xe65b00e46cedd0aa", 2], + ["0x42e62be4a39e5b60", 1], + ["0x806df4ccaa9ed485", 1], + ["0x8375104b299b74c5", 1], + ["0x5d1fbfbe852f2807", 1], + ["0xc6886e2f8e598b0a", 1], + ], + "authoringVersion": 1, + "implName": "node-subtensor", + "implVersion": 1, + "specName": "node-subtensor", + "specVersion": 208, + "stateVersion": 1, + "transactionVersion": 1, + }, + }, + }, + "state_getStorageAt": { + '["0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a431700", null]': { + "jsonrpc": "2.0", + "result": "0x01", + }, + '["0x658faa385070e074c85bf6b568cf05555cd1c97edf92be296fb8ae73ee8611261700", "0x056fb97cc61bb3892f5c1f58574cd8a489ccfb7681079137b7bb2f7aaaaeef35"]': { + "jsonrpc": "2.0", + "result": "0x0100", + }, + }, + "system_chain": {"[]": {"jsonrpc": "2.0", "result": "Bittensor"}}, + }, + "neuron_for_uid": { + "neuronInfo_getNeuron": { + "[23, 5]": { + "jsonrpc": "2.0", + "result": [ + 74, + 247, + 89, + 137, + 82, + 248, + 118, + 131, + 32, + 74, + 42, + 12, + 34, + 102, + 249, + 65, + 167, + 72, + 153, + 189, + 105, + 176, + 206, + 86, + 228, + 42, + 130, + 130, + 15, + 104, + 148, + 70, + 18, + 87, + 222, + 135, + 167, + 26, + 187, + 22, + 68, + 3, + 247, + 203, + 158, + 166, + 93, + 7, + 33, + 248, + 187, + 137, + 216, + 189, + 133, + 117, + 77, + 236, + 238, + 221, + 145, + 232, + 96, + 83, + 20, + 92, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 4, + 18, + 87, + 222, + 135, + 167, + 26, + 187, + 22, + 68, + 3, + 247, + 203, + 158, + 166, + 93, + 7, + 33, + 248, + 187, + 137, + 216, + 189, + 133, + 117, + 77, + 236, + 238, + 221, + 145, + 232, + 96, + 83, + 3, + 176, + 236, + 104, + 179, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 110, + 68, + 120, + 0, + 1, + 0, + 0, + 0, + ], + } + }, + "system_chain": {"[]": {"jsonrpc": "2.0", "result": "Bittensor"}}, + }, + "neurons": { + "chain_getHead": { + "[]": { + "jsonrpc": "2.0", + "result": "0x747c46b08af3b7bbfb04647b4befdf7be9c58946e1a01b171af76bdc5d37cce6", + } + }, + "chain_getHeader": { + '["0x67adc1cae2bb149f9cd5042966bb0ee02978dd600fcb64edca67c4cfa2650cc7"]': { + "jsonrpc": "2.0", + "result": { + "digest": { + "logs": [ + "0x066175726120a8489a0800000000", + "0x0466726f6e880187ae61a3c88cefd9748ac20356acfab7cc8ee7a809a8ccff1d2b4f27efbc4ec800", + "0x05617572610101ecea1a230a86f29a08c727797ca0320dfcffe892089655eb742fbb2af8afaa62ffa52191fa8cfc2066722e7cf61ba14436614b2ef6cc6019f416a321f5dfa781", + ] + }, + "extrinsicsRoot": "0x6eb4c3f270941a592853302593ef2aac1f0f6582fe9cb40207d18a30f7c47622", + "number": "0x31ce9b", + "parentHash": "0xba206b08f55118c21e6e243702052edcecd48e89d951fa05213adde358473da0", + "stateRoot": "0x717d4b05102fa27491f18cef483dda197876a56e8e84f0dd055c107a4b6d2e62", + }, + }, + '["0x747c46b08af3b7bbfb04647b4befdf7be9c58946e1a01b171af76bdc5d37cce6"]': { + "jsonrpc": "2.0", + "result": { + "digest": { + "logs": [ + "0x066175726120a9489a0800000000", + "0x0466726f6e8801b95cd20cae558f77b52bec97040fe30e43283fd8e6762e612a238cd7ce60fa9400", + "0x05617572610101ec524faed02a7e4e36f5bfda3278bbe9b01737d80fb2083ec413c3575095951bdc2351f10348484cda77d8f7da00b8d23fe100732623a9d2aae659422cb5888a", + ] + }, + "extrinsicsRoot": "0xc407703dcfbe36ec23ce316c0c7d8d0b3db02c97359e70400c28e64dbdf95910", + "number": "0x31ce9c", + "parentHash": "0x67adc1cae2bb149f9cd5042966bb0ee02978dd600fcb64edca67c4cfa2650cc7", + "stateRoot": "0x9a9734fc313a151d000a910f34d3c538a42c6d5b9043d49acfdc15b48439690c", + }, + }, + }, + "rpc_methods": { + "[]": { + "jsonrpc": "2.0", + "result": { + "methods": [ + "account_nextIndex", + "archive_unstable_body", + "archive_unstable_call", + "archive_unstable_finalizedHeight", + "archive_unstable_genesisHash", + "archive_unstable_hashByHeight", + "archive_unstable_header", + "archive_unstable_storage", + "author_hasKey", + "author_hasSessionKeys", + "author_insertKey", + "author_pendingExtrinsics", + "author_removeExtrinsic", + "author_rotateKeys", + "author_submitAndWatchExtrinsic", + "author_submitExtrinsic", + "author_unwatchExtrinsic", + "chainHead_v1_body", + "chainHead_v1_call", + "chainHead_v1_continue", + "chainHead_v1_follow", + "chainHead_v1_header", + "chainHead_v1_stopOperation", + "chainHead_v1_storage", + "chainHead_v1_unfollow", + "chainHead_v1_unpin", + "chainSpec_v1_chainName", + "chainSpec_v1_genesisHash", + "chainSpec_v1_properties", + "chain_getBlock", + "chain_getBlockHash", + "chain_getFinalisedHead", + "chain_getFinalizedHead", + "chain_getHead", + "chain_getHeader", + "chain_getRuntimeVersion", + "chain_subscribeAllHeads", + "chain_subscribeFinalisedHeads", + "chain_subscribeFinalizedHeads", + "chain_subscribeNewHead", + "chain_subscribeNewHeads", + "chain_subscribeRuntimeVersion", + "chain_unsubscribeAllHeads", + "chain_unsubscribeFinalisedHeads", + "chain_unsubscribeFinalizedHeads", + "chain_unsubscribeNewHead", + "chain_unsubscribeNewHeads", + "chain_unsubscribeRuntimeVersion", + "childstate_getKeys", + "childstate_getKeysPaged", + "childstate_getKeysPagedAt", + "childstate_getStorage", + "childstate_getStorageEntries", + "childstate_getStorageHash", + "childstate_getStorageSize", + "debug_getBadBlocks", + "debug_getRawBlock", + "debug_getRawHeader", + "debug_getRawReceipts", + "debug_getRawTransaction", + "delegateInfo_getDelegate", + "delegateInfo_getDelegated", + "delegateInfo_getDelegates", + "eth_accounts", + "eth_blockNumber", + "eth_call", + "eth_chainId", + "eth_coinbase", + "eth_estimateGas", + "eth_feeHistory", + "eth_gasPrice", + "eth_getBalance", + "eth_getBlockByHash", + "eth_getBlockByNumber", + "eth_getBlockReceipts", + "eth_getBlockTransactionCountByHash", + "eth_getBlockTransactionCountByNumber", + "eth_getCode", + "eth_getFilterChanges", + "eth_getFilterLogs", + "eth_getLogs", + "eth_getStorageAt", + "eth_getTransactionByBlockHashAndIndex", + "eth_getTransactionByBlockNumberAndIndex", + "eth_getTransactionByHash", + "eth_getTransactionCount", + "eth_getTransactionReceipt", + "eth_getUncleByBlockHashAndIndex", + "eth_getUncleByBlockNumberAndIndex", + "eth_getUncleCountByBlockHash", + "eth_getUncleCountByBlockNumber", + "eth_getWork", + "eth_hashrate", + "eth_maxPriorityFeePerGas", + "eth_mining", + "eth_newBlockFilter", + "eth_newFilter", + "eth_newPendingTransactionFilter", + "eth_protocolVersion", + "eth_sendRawTransaction", + "eth_sendTransaction", + "eth_submitHashrate", + "eth_submitWork", + "eth_subscribe", + "eth_syncing", + "eth_uninstallFilter", + "eth_unsubscribe", + "net_listening", + "net_peerCount", + "net_version", + "neuronInfo_getNeuron", + "neuronInfo_getNeuronLite", + "neuronInfo_getNeurons", + "neuronInfo_getNeuronsLite", + "offchain_localStorageGet", + "offchain_localStorageSet", + "payment_queryFeeDetails", + "payment_queryInfo", + "rpc_methods", + "state_call", + "state_callAt", + "state_getChildReadProof", + "state_getKeys", + "state_getKeysPaged", + "state_getKeysPagedAt", + "state_getMetadata", + "state_getPairs", + "state_getReadProof", + "state_getRuntimeVersion", + "state_getStorage", + "state_getStorageAt", + "state_getStorageHash", + "state_getStorageHashAt", + "state_getStorageSize", + "state_getStorageSizeAt", + "state_queryStorage", + "state_queryStorageAt", + "state_subscribeRuntimeVersion", + "state_subscribeStorage", + "state_traceBlock", + "state_unsubscribeRuntimeVersion", + "state_unsubscribeStorage", + "subnetInfo_getLockCost", + "subnetInfo_getSubnetHyperparams", + "subnetInfo_getSubnetInfo", + "subnetInfo_getSubnetInfo_v2", + "subnetInfo_getSubnetsInf_v2", + "subnetInfo_getSubnetsInfo", + "subscribe_newHead", + "system_accountNextIndex", + "system_addLogFilter", + "system_addReservedPeer", + "system_chain", + "system_chainType", + "system_dryRun", + "system_dryRunAt", + "system_health", + "system_localListenAddresses", + "system_localPeerId", + "system_name", + "system_nodeRoles", + "system_peers", + "system_properties", + "system_removeReservedPeer", + "system_reservedPeers", + "system_resetLogFilter", + "system_syncState", + "system_unstable_networkState", + "system_version", + "transactionWatch_v1_submitAndWatch", + "transactionWatch_v1_unwatch", + "transaction_v1_broadcast", + "transaction_v1_stop", + "unsubscribe_newHead", + "web3_clientVersion", + "web3_sha3", + ] + }, + } + }, + "state_call": { + '["NeuronInfoRuntimeApi_get_neurons_lite", "0x1700"]': { + "jsonrpc": "2.0", + "result": "0x35364cb26667d9765b6aaa76c1b7f0f2786c03257a090081bb483b1e6862e409f64e3f6aa1c0384f95f3ec8f764309523a653a5bd814632fe2eff931d35c606a5e0c65005c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000046aa1c0384f95f3ec8f764309523a653a5bd814632fe2eff931d35c606a5e0c65620bb70000000000000000825b53000100c0dcbc2b619e07312e6fa2e1b9c91eb6826c77ab732fa1670bfa175c3cc06b01d89c740d9dfede764de5284c89dcedac74aa23b2d303a668e251620b78d79949045c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004d89c740d9dfede764de5284c89dcedac74aa23b2d303a668e251620b78d7994907f1a79e0402000000000000003aac57000100e6e23c10ab6ed114577d8fa56f976d350aabea07e0382c6ab0313f1fd5b4ed7c9aa545ce482f48c3160fa706bbafe923bb8248403141729d95c96699b615f04d085c00e390160000000000b40200001329d44300000000000000000000000049080404000000000000000000000000000000000000000000000000000000000000000000049aa545ce482f48c3160fa706bbafe923bb8248403141729d95c96699b615f04d000000000000000022435a000100866eda07c029ea2856de08c183ddff461cb6ce83f8ec49150ef5647eee8f4c6b40469c545b3d0396a76ca1c41a6c090266e2e532adc9fde080d9e90d2e7b093d0c5c00dfb8190000000000b5020000026fb5d50000000000000000000000009aac04040000000000000000000000000000000000000000000000000000000000000000000440469c545b3d0396a76ca1c41a6c090266e2e532adc9fde080d9e90d2e7b093dea2b0e000000000000000046d9660001000cc6dd507099d72736a92d6f889e4024e3431ac57eb9eb1fe0ca3302c6d4c867509b91e2cf336358ec2f3fe6e0598e2909c29a57597765240dfa8db41e7dd46a105c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004509b91e2cf336358ec2f3fe6e0598e2909c29a57597765240dfa8db41e7dd46a00000000000000005a9f6f0001004af7598952f87683204a2a0c2266f941a74899bd69b0ce56e42a82820f6894461257de87a71abb164403f7cb9ea65d0721f8bb89d8bd85754deceedd91e86053145c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000041257de87a71abb164403f7cb9ea65d0721f8bb89d8bd85754deceedd91e8605303b0ec68b3000000000000006e4478000100ba7c215ab878d9231d11bdaa5e4717b90d3b85df986abc669de607b1da6a020c74d879f5f872947565597d2f0c310fa0658d168ecd68a2ad3d5ec63438561643185c00265e1e0000000000b5020000583937a2000000000000000000000000d54f04040000000000000000000000000000000000000000000000000000000000000000000474d879f5f872947565597d2f0c310fa0658d168ecd68a2ad3d5ec6343856164307fd5a63c402000000000000004a7579000100084667e9ab96aca6c12c3094d740142cc4d36e423ede27b752978ffc70841b4dda618f84f833dc38b54f31cca63c408134bcce60f38c16c2f64fd4cf37068b0f1c5c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004da618f84f833dc38b54f31cca63c408134bcce60f38c16c2f64fd4cf37068b0f07bfb78a48170000000000000022e888000100f4b1b7c1237c118a43504f0b1f312c027fb7517cb46911d9a9ab1c432be0b039da618f84f833dc38b54f31cca63c408134bcce60f38c16c2f64fd4cf37068b0f205c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004da618f84f833dc38b54f31cca63c408134bcce60f38c16c2f64fd4cf37068b0f0753238a4817000000000000003ae888000100446ad8dfc37a54c8c1030e70eedd53b7b410cec1376a0aae42fd376b9749272f002326240a609c72641c6a3fcad3ad2ee33f2db95f6c16e181f7c62fa063834b245c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004002326240a609c72641c6a3fcad3ad2ee33f2db95f6c16e181f7c62fa063834b07fcbacd2e0200000000000000eec2890001008a90be061598f4b592afbd546bcb6beadb3c02f5c129df2e11b698f9543dbd412aa58acc7df6cea78de0928a5c6c2e79f3d24e5893d6a5971738cfbc03ca8f33285c010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000042aa58acc7df6cea78de0928a5c6c2e79f3d24e5893d6a5971738cfbc03ca8f330f65f486c4ba2401000000000000007a32c70001feff03000efdb10e8dfe2e73b6f29ce09aaad2c374dc22e2a64fcad4a77ed547e9175a08de74e52b42323230d99ec6f46fc1bf01b03f54f11f02fddb9d9668535e8ed4632c5c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004de74e52b42323230d99ec6f46fc1bf01b03f54f11f02fddb9d9668535e8ed46300000000000000002e0791000100984a52dfdfbba392625d238ff859ec78d79b04f8ad456c9ab8d8426a085d4e73de74e52b42323230d99ec6f46fc1bf01b03f54f11f02fddb9d9668535e8ed463305c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004de74e52b42323230d99ec6f46fc1bf01b03f54f11f02fddb9d9668535e8ed46307b7ec18d01f00000000000000720791000100c260f03b11e0e67574deb81febf89f6afb697d6c36d165f4ecb65522ea433805de74e52b42323230d99ec6f46fc1bf01b03f54f11f02fddb9d9668535e8ed463345c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004de74e52b42323230d99ec6f46fc1bf01b03f54f11f02fddb9d9668535e8ed4630000000000000000ae079100010024126de9392aa70486873b23182a00ee1b9e2f394a46d409230871113a76be56b88597476f139767a85863d096b7668620bcf7cb531a950006f18b26d4b30600385c00792d2b0000000000b60200004a024d940000000000000000000000009756040400000000000000000000000000000000000000000000000000000000000000000004b88597476f139767a85863d096b7668620bcf7cb531a950006f18b26d4b306000771d8c05f520000000000000046899400010062380cd8c9bbe606cfda6572b6da580ec035ec57b18dc3d64acbcef53c3efc71143906b4ea1ab0986976c98468b8371bd54cba3330c0e895a1762d8e96c12c023c5c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004143906b4ea1ab0986976c98468b8371bd54cba3330c0e895a1762d8e96c12c024a930c0000000000000000568da100010006c112ee93775bb184b6bf00f22adee24091efe725c249db3ba01b15274b2a2f9ef89253596e8764aecef9a452db36da8188153e0f707f1a0a37011583b5e82d405c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000049ef89253596e8764aecef9a452db36da8188153e0f707f1a0a37011583b5e82d07e68ca9693100000000000000d257ad000100e45433ff9820f9d62b98feff2a95281d14ff6028a9b3c78063a11bbd0fc7bd549ef89253596e8764aecef9a452db36da8188153e0f707f1a0a37011583b5e82d445c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000049ef89253596e8764aecef9a452db36da8188153e0f707f1a0a37011583b5e82d5a9c11a7000000000000004a59ad000100fa01f3e6d839007cc8179e6cb40fb87d4b87502315568340cc51255d4b526d59b24a20c37c76fe89b0ab49ef5ef0e5ceeb33964ef89a496944e9b31cb4915d1b485c004bc72d0000000000b60200008cbc4c94000000000000000000000000464a040400000000000000000000000000000000000000000000000000000000000000000004b24a20c37c76fe89b0ab49ef5ef0e5ceeb33964ef89a496944e9b31cb4915d1bd51500000000000000eedfb4000100", + } + }, + "state_getKeysPaged": { + '["0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab01700", 100, "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab01700", "0x747c46b08af3b7bbfb04647b4befdf7be9c58946e1a01b171af76bdc5d37cce6"]': { + "jsonrpc": "2.0", + "result": [ + "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000000", + "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000100", + "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000200", + "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000300", + "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000400", + "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000500", + "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000600", + "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000700", + "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000800", + "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000900", + "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000a00", + "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000b00", + "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000c00", + "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000d00", + "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000e00", + "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000f00", + "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017001000", + "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017001100", + "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017001200", + ], + }, + '["0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab01700", 100, "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017001200", "0x747c46b08af3b7bbfb04647b4befdf7be9c58946e1a01b171af76bdc5d37cce6"]': { + "jsonrpc": "2.0", + "result": [], + }, + '["0x658faa385070e074c85bf6b568cf0555a1e7036061f484bc3048a64b64e35ec41700", 100, "0x658faa385070e074c85bf6b568cf0555a1e7036061f484bc3048a64b64e35ec41700", "0x67adc1cae2bb149f9cd5042966bb0ee02978dd600fcb64edca67c4cfa2650cc7"]': { + "jsonrpc": "2.0", + "result": [ + "0x658faa385070e074c85bf6b568cf0555a1e7036061f484bc3048a64b64e35ec417000a00" + ], + }, + '["0x658faa385070e074c85bf6b568cf0555a1e7036061f484bc3048a64b64e35ec41700", 100, "0x658faa385070e074c85bf6b568cf0555a1e7036061f484bc3048a64b64e35ec417000a00", "0x67adc1cae2bb149f9cd5042966bb0ee02978dd600fcb64edca67c4cfa2650cc7"]': { + "jsonrpc": "2.0", + "result": [], + }, + }, + "state_getRuntimeVersion": { + '["0x67adc1cae2bb149f9cd5042966bb0ee02978dd600fcb64edca67c4cfa2650cc7"]': { + "jsonrpc": "2.0", + "result": { + "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", 5], + ["0xe65b00e46cedd0aa", 2], + ["0x42e62be4a39e5b60", 1], + ["0x806df4ccaa9ed485", 1], + ["0x8375104b299b74c5", 1], + ["0x5d1fbfbe852f2807", 1], + ["0xc6886e2f8e598b0a", 1], + ], + "authoringVersion": 1, + "implName": "node-subtensor", + "implVersion": 1, + "specName": "node-subtensor", + "specVersion": 208, + "stateVersion": 1, + "transactionVersion": 1, + }, + }, + '["0xba206b08f55118c21e6e243702052edcecd48e89d951fa05213adde358473da0"]': { + "jsonrpc": "2.0", + "result": { + "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", 5], + ["0xe65b00e46cedd0aa", 2], + ["0x42e62be4a39e5b60", 1], + ["0x806df4ccaa9ed485", 1], + ["0x8375104b299b74c5", 1], + ["0x5d1fbfbe852f2807", 1], + ["0xc6886e2f8e598b0a", 1], + ], + "authoringVersion": 1, + "implName": "node-subtensor", + "implVersion": 1, + "specName": "node-subtensor", + "specVersion": 208, + "stateVersion": 1, + "transactionVersion": 1, + }, + }, + }, + "state_queryStorageAt": { + '[["0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000000", "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000100", "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000200", "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000300", "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000400", "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000500", "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000600", "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000700", "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000800", "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000900", "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000a00", "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000b00", "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000c00", "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000d00", "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000e00", "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000f00", "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017001000", "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017001100", "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017001200"], "0x747c46b08af3b7bbfb04647b4befdf7be9c58946e1a01b171af76bdc5d37cce6"]': { + "jsonrpc": "2.0", + "result": [ + { + "block": "0x747c46b08af3b7bbfb04647b4befdf7be9c58946e1a01b171af76bdc5d37cce6", + "changes": [ + [ + "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000000", + "0x00", + ], + [ + "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000100", + "0x00", + ], + [ + "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000200", + "0x00", + ], + [ + "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000300", + "0x00", + ], + [ + "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000400", + "0x00", + ], + [ + "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000500", + "0x00", + ], + [ + "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000600", + "0x00", + ], + [ + "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000700", + "0x00", + ], + [ + "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000800", + "0x00", + ], + [ + "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000900", + "0x00", + ], + [ + "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000a00", + "0x00", + ], + [ + "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000b00", + "0x00", + ], + [ + "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000c00", + "0x00", + ], + [ + "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000d00", + "0x00", + ], + [ + "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000e00", + "0x00", + ], + [ + "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017000f00", + "0x00", + ], + [ + "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017001000", + "0x00", + ], + [ + "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017001100", + "0x00", + ], + [ + "0x658faa385070e074c85bf6b568cf05555641285262b8f970e4cbaafbff8d6ab017001200", + "0x00", + ], + ], + } + ], + }, + '[["0x658faa385070e074c85bf6b568cf0555a1e7036061f484bc3048a64b64e35ec417000a00"], "0x67adc1cae2bb149f9cd5042966bb0ee02978dd600fcb64edca67c4cfa2650cc7"]': { + "jsonrpc": "2.0", + "result": [ + { + "block": "0x67adc1cae2bb149f9cd5042966bb0ee02978dd600fcb64edca67c4cfa2650cc7", + "changes": [ + [ + "0x658faa385070e074c85bf6b568cf0555a1e7036061f484bc3048a64b64e35ec417000a00", + "0x040a00ffff", + ] + ], + } + ], + }, + }, + "system_chain": {"[]": {"jsonrpc": "2.0", "result": "Bittensor"}}, + }, + "neurons_lite": { + "chain_getHead": { + "[]": { + "jsonrpc": "2.0", + "result": "0xd867f39bb9e2a626945183af01065b61d0695d435a765bfd0ce5f821fbed8f82", + } + }, + "chain_getHeader": { + '["0xd867f39bb9e2a626945183af01065b61d0695d435a765bfd0ce5f821fbed8f82"]': { + "jsonrpc": "2.0", + "result": { + "digest": { + "logs": [ + "0x066175726120aa489a0800000000", + "0x0466726f6e88011a69331481627271f5f40c4f35ebffac418255ef2257753a01e865193b2c4ee700", + "0x056175726101016edac58b21702011943c42db257af0026b7952343e03c341979d3d9414cd970e4e07a26708f8a6e66e9a46d3f440b40988a6ccdef3d8329fd7399faaa20cb185", + ] + }, + "extrinsicsRoot": "0xdb0d49424c84fec28943ea69ecdc5325f40f9c798a844c6e1e2313cf385d8571", + "number": "0x31ce9d", + "parentHash": "0x747c46b08af3b7bbfb04647b4befdf7be9c58946e1a01b171af76bdc5d37cce6", + "stateRoot": "0x161979ee1f3ab18ab09843502bdc05d43185465ac6008eb6cecc2104de848bf5", + }, + } + }, + "rpc_methods": { + "[]": { + "jsonrpc": "2.0", + "result": { + "methods": [ + "account_nextIndex", + "archive_unstable_body", + "archive_unstable_call", + "archive_unstable_finalizedHeight", + "archive_unstable_genesisHash", + "archive_unstable_hashByHeight", + "archive_unstable_header", + "archive_unstable_storage", + "author_hasKey", + "author_hasSessionKeys", + "author_insertKey", + "author_pendingExtrinsics", + "author_removeExtrinsic", + "author_rotateKeys", + "author_submitAndWatchExtrinsic", + "author_submitExtrinsic", + "author_unwatchExtrinsic", + "chainHead_v1_body", + "chainHead_v1_call", + "chainHead_v1_continue", + "chainHead_v1_follow", + "chainHead_v1_header", + "chainHead_v1_stopOperation", + "chainHead_v1_storage", + "chainHead_v1_unfollow", + "chainHead_v1_unpin", + "chainSpec_v1_chainName", + "chainSpec_v1_genesisHash", + "chainSpec_v1_properties", + "chain_getBlock", + "chain_getBlockHash", + "chain_getFinalisedHead", + "chain_getFinalizedHead", + "chain_getHead", + "chain_getHeader", + "chain_getRuntimeVersion", + "chain_subscribeAllHeads", + "chain_subscribeFinalisedHeads", + "chain_subscribeFinalizedHeads", + "chain_subscribeNewHead", + "chain_subscribeNewHeads", + "chain_subscribeRuntimeVersion", + "chain_unsubscribeAllHeads", + "chain_unsubscribeFinalisedHeads", + "chain_unsubscribeFinalizedHeads", + "chain_unsubscribeNewHead", + "chain_unsubscribeNewHeads", + "chain_unsubscribeRuntimeVersion", + "childstate_getKeys", + "childstate_getKeysPaged", + "childstate_getKeysPagedAt", + "childstate_getStorage", + "childstate_getStorageEntries", + "childstate_getStorageHash", + "childstate_getStorageSize", + "debug_getBadBlocks", + "debug_getRawBlock", + "debug_getRawHeader", + "debug_getRawReceipts", + "debug_getRawTransaction", + "delegateInfo_getDelegate", + "delegateInfo_getDelegated", + "delegateInfo_getDelegates", + "eth_accounts", + "eth_blockNumber", + "eth_call", + "eth_chainId", + "eth_coinbase", + "eth_estimateGas", + "eth_feeHistory", + "eth_gasPrice", + "eth_getBalance", + "eth_getBlockByHash", + "eth_getBlockByNumber", + "eth_getBlockReceipts", + "eth_getBlockTransactionCountByHash", + "eth_getBlockTransactionCountByNumber", + "eth_getCode", + "eth_getFilterChanges", + "eth_getFilterLogs", + "eth_getLogs", + "eth_getStorageAt", + "eth_getTransactionByBlockHashAndIndex", + "eth_getTransactionByBlockNumberAndIndex", + "eth_getTransactionByHash", + "eth_getTransactionCount", + "eth_getTransactionReceipt", + "eth_getUncleByBlockHashAndIndex", + "eth_getUncleByBlockNumberAndIndex", + "eth_getUncleCountByBlockHash", + "eth_getUncleCountByBlockNumber", + "eth_getWork", + "eth_hashrate", + "eth_maxPriorityFeePerGas", + "eth_mining", + "eth_newBlockFilter", + "eth_newFilter", + "eth_newPendingTransactionFilter", + "eth_protocolVersion", + "eth_sendRawTransaction", + "eth_sendTransaction", + "eth_submitHashrate", + "eth_submitWork", + "eth_subscribe", + "eth_syncing", + "eth_uninstallFilter", + "eth_unsubscribe", + "net_listening", + "net_peerCount", + "net_version", + "neuronInfo_getNeuron", + "neuronInfo_getNeuronLite", + "neuronInfo_getNeurons", + "neuronInfo_getNeuronsLite", + "offchain_localStorageGet", + "offchain_localStorageSet", + "payment_queryFeeDetails", + "payment_queryInfo", + "rpc_methods", + "state_call", + "state_callAt", + "state_getChildReadProof", + "state_getKeys", + "state_getKeysPaged", + "state_getKeysPagedAt", + "state_getMetadata", + "state_getPairs", + "state_getReadProof", + "state_getRuntimeVersion", + "state_getStorage", + "state_getStorageAt", + "state_getStorageHash", + "state_getStorageHashAt", + "state_getStorageSize", + "state_getStorageSizeAt", + "state_queryStorage", + "state_queryStorageAt", + "state_subscribeRuntimeVersion", + "state_subscribeStorage", + "state_traceBlock", + "state_unsubscribeRuntimeVersion", + "state_unsubscribeStorage", + "subnetInfo_getLockCost", + "subnetInfo_getSubnetHyperparams", + "subnetInfo_getSubnetInfo", + "subnetInfo_getSubnetInfo_v2", + "subnetInfo_getSubnetsInf_v2", + "subnetInfo_getSubnetsInfo", + "subscribe_newHead", + "system_accountNextIndex", + "system_addLogFilter", + "system_addReservedPeer", + "system_chain", + "system_chainType", + "system_dryRun", + "system_dryRunAt", + "system_health", + "system_localListenAddresses", + "system_localPeerId", + "system_name", + "system_nodeRoles", + "system_peers", + "system_properties", + "system_removeReservedPeer", + "system_reservedPeers", + "system_resetLogFilter", + "system_syncState", + "system_unstable_networkState", + "system_version", + "transactionWatch_v1_submitAndWatch", + "transactionWatch_v1_unwatch", + "transaction_v1_broadcast", + "transaction_v1_stop", + "unsubscribe_newHead", + "web3_clientVersion", + "web3_sha3", + ] + }, + } + }, + "state_call": { + '["NeuronInfoRuntimeApi_get_neurons_lite", "0x1700"]': { + "jsonrpc": "2.0", + "result": "0x35364cb26667d9765b6aaa76c1b7f0f2786c03257a090081bb483b1e6862e409f64e3f6aa1c0384f95f3ec8f764309523a653a5bd814632fe2eff931d35c606a5e0c65005c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000046aa1c0384f95f3ec8f764309523a653a5bd814632fe2eff931d35c606a5e0c65620bb70000000000000000825b53000100c0dcbc2b619e07312e6fa2e1b9c91eb6826c77ab732fa1670bfa175c3cc06b01d89c740d9dfede764de5284c89dcedac74aa23b2d303a668e251620b78d79949045c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004d89c740d9dfede764de5284c89dcedac74aa23b2d303a668e251620b78d7994907f1a79e0402000000000000003aac57000100e6e23c10ab6ed114577d8fa56f976d350aabea07e0382c6ab0313f1fd5b4ed7c9aa545ce482f48c3160fa706bbafe923bb8248403141729d95c96699b615f04d085c00e390160000000000b40200001329d44300000000000000000000000049080404000000000000000000000000000000000000000000000000000000000000000000049aa545ce482f48c3160fa706bbafe923bb8248403141729d95c96699b615f04d000000000000000022435a000100866eda07c029ea2856de08c183ddff461cb6ce83f8ec49150ef5647eee8f4c6b40469c545b3d0396a76ca1c41a6c090266e2e532adc9fde080d9e90d2e7b093d0c5c00dfb8190000000000b5020000026fb5d50000000000000000000000009aac04040000000000000000000000000000000000000000000000000000000000000000000440469c545b3d0396a76ca1c41a6c090266e2e532adc9fde080d9e90d2e7b093dea2b0e000000000000000046d9660001000cc6dd507099d72736a92d6f889e4024e3431ac57eb9eb1fe0ca3302c6d4c867509b91e2cf336358ec2f3fe6e0598e2909c29a57597765240dfa8db41e7dd46a105c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004509b91e2cf336358ec2f3fe6e0598e2909c29a57597765240dfa8db41e7dd46a00000000000000005a9f6f0001004af7598952f87683204a2a0c2266f941a74899bd69b0ce56e42a82820f6894461257de87a71abb164403f7cb9ea65d0721f8bb89d8bd85754deceedd91e86053145c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000041257de87a71abb164403f7cb9ea65d0721f8bb89d8bd85754deceedd91e8605303b0ec68b3000000000000006e4478000100ba7c215ab878d9231d11bdaa5e4717b90d3b85df986abc669de607b1da6a020c74d879f5f872947565597d2f0c310fa0658d168ecd68a2ad3d5ec63438561643185c00265e1e0000000000b5020000583937a2000000000000000000000000d54f04040000000000000000000000000000000000000000000000000000000000000000000474d879f5f872947565597d2f0c310fa0658d168ecd68a2ad3d5ec6343856164307fd5a63c402000000000000004a7579000100084667e9ab96aca6c12c3094d740142cc4d36e423ede27b752978ffc70841b4dda618f84f833dc38b54f31cca63c408134bcce60f38c16c2f64fd4cf37068b0f1c5c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004da618f84f833dc38b54f31cca63c408134bcce60f38c16c2f64fd4cf37068b0f07bfb78a48170000000000000022e888000100f4b1b7c1237c118a43504f0b1f312c027fb7517cb46911d9a9ab1c432be0b039da618f84f833dc38b54f31cca63c408134bcce60f38c16c2f64fd4cf37068b0f205c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004da618f84f833dc38b54f31cca63c408134bcce60f38c16c2f64fd4cf37068b0f0753238a4817000000000000003ae888000100446ad8dfc37a54c8c1030e70eedd53b7b410cec1376a0aae42fd376b9749272f002326240a609c72641c6a3fcad3ad2ee33f2db95f6c16e181f7c62fa063834b245c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004002326240a609c72641c6a3fcad3ad2ee33f2db95f6c16e181f7c62fa063834b07fcbacd2e0200000000000000eec2890001008a90be061598f4b592afbd546bcb6beadb3c02f5c129df2e11b698f9543dbd412aa58acc7df6cea78de0928a5c6c2e79f3d24e5893d6a5971738cfbc03ca8f33285c010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000042aa58acc7df6cea78de0928a5c6c2e79f3d24e5893d6a5971738cfbc03ca8f330f65f486c4ba2401000000000000007a32c70001feff03000efdb10e8dfe2e73b6f29ce09aaad2c374dc22e2a64fcad4a77ed547e9175a08de74e52b42323230d99ec6f46fc1bf01b03f54f11f02fddb9d9668535e8ed4632c5c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004de74e52b42323230d99ec6f46fc1bf01b03f54f11f02fddb9d9668535e8ed46300000000000000002e0791000100984a52dfdfbba392625d238ff859ec78d79b04f8ad456c9ab8d8426a085d4e73de74e52b42323230d99ec6f46fc1bf01b03f54f11f02fddb9d9668535e8ed463305c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004de74e52b42323230d99ec6f46fc1bf01b03f54f11f02fddb9d9668535e8ed46307b7ec18d01f00000000000000720791000100c260f03b11e0e67574deb81febf89f6afb697d6c36d165f4ecb65522ea433805de74e52b42323230d99ec6f46fc1bf01b03f54f11f02fddb9d9668535e8ed463345c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004de74e52b42323230d99ec6f46fc1bf01b03f54f11f02fddb9d9668535e8ed4630000000000000000ae079100010024126de9392aa70486873b23182a00ee1b9e2f394a46d409230871113a76be56b88597476f139767a85863d096b7668620bcf7cb531a950006f18b26d4b30600385c00792d2b0000000000b60200004a024d940000000000000000000000009756040400000000000000000000000000000000000000000000000000000000000000000004b88597476f139767a85863d096b7668620bcf7cb531a950006f18b26d4b306000771d8c05f520000000000000046899400010062380cd8c9bbe606cfda6572b6da580ec035ec57b18dc3d64acbcef53c3efc71143906b4ea1ab0986976c98468b8371bd54cba3330c0e895a1762d8e96c12c023c5c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004143906b4ea1ab0986976c98468b8371bd54cba3330c0e895a1762d8e96c12c024a930c0000000000000000568da100010006c112ee93775bb184b6bf00f22adee24091efe725c249db3ba01b15274b2a2f9ef89253596e8764aecef9a452db36da8188153e0f707f1a0a37011583b5e82d405c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000049ef89253596e8764aecef9a452db36da8188153e0f707f1a0a37011583b5e82d07e68ca9693100000000000000d257ad000100e45433ff9820f9d62b98feff2a95281d14ff6028a9b3c78063a11bbd0fc7bd549ef89253596e8764aecef9a452db36da8188153e0f707f1a0a37011583b5e82d445c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000049ef89253596e8764aecef9a452db36da8188153e0f707f1a0a37011583b5e82d5a9c11a7000000000000004a59ad000100fa01f3e6d839007cc8179e6cb40fb87d4b87502315568340cc51255d4b526d59b24a20c37c76fe89b0ab49ef5ef0e5ceeb33964ef89a496944e9b31cb4915d1b485c004bc72d0000000000b60200008cbc4c94000000000000000000000000464a040400000000000000000000000000000000000000000000000000000000000000000004b24a20c37c76fe89b0ab49ef5ef0e5ceeb33964ef89a496944e9b31cb4915d1bd51500000000000000eedfb4000100", + } + }, + "state_getRuntimeVersion": { + '["0x747c46b08af3b7bbfb04647b4befdf7be9c58946e1a01b171af76bdc5d37cce6"]': { + "jsonrpc": "2.0", + "result": { + "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", 5], + ["0xe65b00e46cedd0aa", 2], + ["0x42e62be4a39e5b60", 1], + ["0x806df4ccaa9ed485", 1], + ["0x8375104b299b74c5", 1], + ["0x5d1fbfbe852f2807", 1], + ["0xc6886e2f8e598b0a", 1], + ], + "authoringVersion": 1, + "implName": "node-subtensor", + "implVersion": 1, + "specName": "node-subtensor", + "specVersion": 208, + "stateVersion": 1, + "transactionVersion": 1, + }, + } + }, + "system_chain": {"[]": {"jsonrpc": "2.0", "result": "Bittensor"}}, + }, + "recycle": { + "chain_getHead": { + "[]": { + "jsonrpc": "2.0", + "result": "0x78fcbb9531d6f1032b78164d980f79994f4321d8bdd2e1446696db38b6be9906", + } + }, + "chain_getHeader": { + '["0x78fcbb9531d6f1032b78164d980f79994f4321d8bdd2e1446696db38b6be9906"]': { + "jsonrpc": "2.0", + "result": { + "digest": { + "logs": [ + "0x066175726120af489a0800000000", + "0x0466726f6e88019e6b8c3ee8e78fdae0e9837c9233a72d4dae4a78a502619f3136a9944dcfd31600", + "0x056175726101017ed9e3fadc29c95e5982ab7126e1274cabb953d101256ec0b99db2f4ce266d658ed03fd0ec8ed66c686874d3a668e3fe38562c3fd51e378e10a6f00dfa3b1f82", + ] + }, + "extrinsicsRoot": "0xbd59ea9aed82ecde226b70a90b38690562e7d0ef2e0bc8c6a70d297a39c0f4f3", + "number": "0x31cea2", + "parentHash": "0xef1f94e2a46fbd2b39f60d855da491ee6e2637d9c6092358f8ce97b6839973ae", + "stateRoot": "0xdfa23783ec2405ae41f8d17add8e1fc2c415042425d30f43411b8be6f37775b5", + }, + }, + '["0xef1f94e2a46fbd2b39f60d855da491ee6e2637d9c6092358f8ce97b6839973ae"]': { + "jsonrpc": "2.0", + "result": { + "digest": { + "logs": [ + "0x066175726120ae489a0800000000", + "0x0466726f6e8801e6315ae1138f6d998b7a67af7473bf3f811791a5427fd0335d33b140f79e811800", + "0x05617572610101c2b3d49809ccc446a0237b04b8429b8a92bf9555506897b406c605008efc28755adb7f5a261017006a386f401497e6edda9e417aca0cf2d6bd81d6dd2b86c08d", + ] + }, + "extrinsicsRoot": "0xdefbb162adbbecaa3a3cad493c55ee69556b194ece6893f711d689704743580f", + "number": "0x31cea1", + "parentHash": "0xa0ee9e0d47444a8570eec6e4e48120bba7aef6fcce277d90cce6d48655398459", + "stateRoot": "0x84c348ee245bf2ce1869223dfcadb5d2a37331d5f00dcef879402de2a0f4c0bd", + }, + }, + }, + "rpc_methods": { + "[]": { + "jsonrpc": "2.0", + "result": { + "methods": [ + "account_nextIndex", + "archive_unstable_body", + "archive_unstable_call", + "archive_unstable_finalizedHeight", + "archive_unstable_genesisHash", + "archive_unstable_hashByHeight", + "archive_unstable_header", + "archive_unstable_storage", + "author_hasKey", + "author_hasSessionKeys", + "author_insertKey", + "author_pendingExtrinsics", + "author_removeExtrinsic", + "author_rotateKeys", + "author_submitAndWatchExtrinsic", + "author_submitExtrinsic", + "author_unwatchExtrinsic", + "chainHead_v1_body", + "chainHead_v1_call", + "chainHead_v1_continue", + "chainHead_v1_follow", + "chainHead_v1_header", + "chainHead_v1_stopOperation", + "chainHead_v1_storage", + "chainHead_v1_unfollow", + "chainHead_v1_unpin", + "chainSpec_v1_chainName", + "chainSpec_v1_genesisHash", + "chainSpec_v1_properties", + "chain_getBlock", + "chain_getBlockHash", + "chain_getFinalisedHead", + "chain_getFinalizedHead", + "chain_getHead", + "chain_getHeader", + "chain_getRuntimeVersion", + "chain_subscribeAllHeads", + "chain_subscribeFinalisedHeads", + "chain_subscribeFinalizedHeads", + "chain_subscribeNewHead", + "chain_subscribeNewHeads", + "chain_subscribeRuntimeVersion", + "chain_unsubscribeAllHeads", + "chain_unsubscribeFinalisedHeads", + "chain_unsubscribeFinalizedHeads", + "chain_unsubscribeNewHead", + "chain_unsubscribeNewHeads", + "chain_unsubscribeRuntimeVersion", + "childstate_getKeys", + "childstate_getKeysPaged", + "childstate_getKeysPagedAt", + "childstate_getStorage", + "childstate_getStorageEntries", + "childstate_getStorageHash", + "childstate_getStorageSize", + "debug_getBadBlocks", + "debug_getRawBlock", + "debug_getRawHeader", + "debug_getRawReceipts", + "debug_getRawTransaction", + "delegateInfo_getDelegate", + "delegateInfo_getDelegated", + "delegateInfo_getDelegates", + "eth_accounts", + "eth_blockNumber", + "eth_call", + "eth_chainId", + "eth_coinbase", + "eth_estimateGas", + "eth_feeHistory", + "eth_gasPrice", + "eth_getBalance", + "eth_getBlockByHash", + "eth_getBlockByNumber", + "eth_getBlockReceipts", + "eth_getBlockTransactionCountByHash", + "eth_getBlockTransactionCountByNumber", + "eth_getCode", + "eth_getFilterChanges", + "eth_getFilterLogs", + "eth_getLogs", + "eth_getStorageAt", + "eth_getTransactionByBlockHashAndIndex", + "eth_getTransactionByBlockNumberAndIndex", + "eth_getTransactionByHash", + "eth_getTransactionCount", + "eth_getTransactionReceipt", + "eth_getUncleByBlockHashAndIndex", + "eth_getUncleByBlockNumberAndIndex", + "eth_getUncleCountByBlockHash", + "eth_getUncleCountByBlockNumber", + "eth_getWork", + "eth_hashrate", + "eth_maxPriorityFeePerGas", + "eth_mining", + "eth_newBlockFilter", + "eth_newFilter", + "eth_newPendingTransactionFilter", + "eth_protocolVersion", + "eth_sendRawTransaction", + "eth_sendTransaction", + "eth_submitHashrate", + "eth_submitWork", + "eth_subscribe", + "eth_syncing", + "eth_uninstallFilter", + "eth_unsubscribe", + "net_listening", + "net_peerCount", + "net_version", + "neuronInfo_getNeuron", + "neuronInfo_getNeuronLite", + "neuronInfo_getNeurons", + "neuronInfo_getNeuronsLite", + "offchain_localStorageGet", + "offchain_localStorageSet", + "payment_queryFeeDetails", + "payment_queryInfo", + "rpc_methods", + "state_call", + "state_callAt", + "state_getChildReadProof", + "state_getKeys", + "state_getKeysPaged", + "state_getKeysPagedAt", + "state_getMetadata", + "state_getPairs", + "state_getReadProof", + "state_getRuntimeVersion", + "state_getStorage", + "state_getStorageAt", + "state_getStorageHash", + "state_getStorageHashAt", + "state_getStorageSize", + "state_getStorageSizeAt", + "state_queryStorage", + "state_queryStorageAt", + "state_subscribeRuntimeVersion", + "state_subscribeStorage", + "state_traceBlock", + "state_unsubscribeRuntimeVersion", + "state_unsubscribeStorage", + "subnetInfo_getLockCost", + "subnetInfo_getSubnetHyperparams", + "subnetInfo_getSubnetInfo", + "subnetInfo_getSubnetInfo_v2", + "subnetInfo_getSubnetsInf_v2", + "subnetInfo_getSubnetsInfo", + "subscribe_newHead", + "system_accountNextIndex", + "system_addLogFilter", + "system_addReservedPeer", + "system_chain", + "system_chainType", + "system_dryRun", + "system_dryRunAt", + "system_health", + "system_localListenAddresses", + "system_localPeerId", + "system_name", + "system_nodeRoles", + "system_peers", + "system_properties", + "system_removeReservedPeer", + "system_reservedPeers", + "system_resetLogFilter", + "system_syncState", + "system_unstable_networkState", + "system_version", + "transactionWatch_v1_submitAndWatch", + "transactionWatch_v1_unwatch", + "transaction_v1_broadcast", + "transaction_v1_stop", + "unsubscribe_newHead", + "web3_clientVersion", + "web3_sha3", + ] + }, + } + }, + "state_getRuntimeVersion": { + '["0xa0ee9e0d47444a8570eec6e4e48120bba7aef6fcce277d90cce6d48655398459"]': { + "jsonrpc": "2.0", + "result": { + "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", 5], + ["0xe65b00e46cedd0aa", 2], + ["0x42e62be4a39e5b60", 1], + ["0x806df4ccaa9ed485", 1], + ["0x8375104b299b74c5", 1], + ["0x5d1fbfbe852f2807", 1], + ["0xc6886e2f8e598b0a", 1], + ], + "authoringVersion": 1, + "implName": "node-subtensor", + "implVersion": 1, + "specName": "node-subtensor", + "specVersion": 208, + "stateVersion": 1, + "transactionVersion": 1, + }, + }, + '["0xef1f94e2a46fbd2b39f60d855da491ee6e2637d9c6092358f8ce97b6839973ae"]': { + "jsonrpc": "2.0", + "result": { + "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", 5], + ["0xe65b00e46cedd0aa", 2], + ["0x42e62be4a39e5b60", 1], + ["0x806df4ccaa9ed485", 1], + ["0x8375104b299b74c5", 1], + ["0x5d1fbfbe852f2807", 1], + ["0xc6886e2f8e598b0a", 1], + ], + "authoringVersion": 1, + "implName": "node-subtensor", + "implVersion": 1, + "specName": "node-subtensor", + "specVersion": 208, + "stateVersion": 1, + "transactionVersion": 1, + }, + }, + }, + "state_getStorageAt": { + '["0x658faa385070e074c85bf6b568cf055501be1755d08418802946bca51b6863251700", "0x78fcbb9531d6f1032b78164d980f79994f4321d8bdd2e1446696db38b6be9906"]': { + "jsonrpc": "2.0", + "result": "0x0100000000000000", + }, + '["0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a431700", null]': { + "jsonrpc": "2.0", + "result": "0x01", + }, + }, + "system_chain": {"[]": {"jsonrpc": "2.0", "result": "Bittensor"}}, + }, + "subnet_exists": { + "chain_getHead": { + "[]": { + "jsonrpc": "2.0", + "result": "0xf305f795b37cb91dbe8369cf67c0badc41dae5c15811ecfdad5fc101f3cc17f5", + } + }, + "chain_getHeader": { + '["0xf305f795b37cb91dbe8369cf67c0badc41dae5c15811ecfdad5fc101f3cc17f5"]': { + "jsonrpc": "2.0", + "result": { + "digest": { + "logs": [ + "0x066175726120a6489a0800000000", + "0x0466726f6e8801e86843cec76184aecb7aaf69b884447f83148b115de587bea2db93e51ba4967c00", + "0x05617572610101b2ad5d9ff2e0abcfaae0f21b718b0eb00ec4f0d0ba10d64b39e2b626e27ea22e3a9d15e550ecc5db3de897d3108a1c73d5b003a7c5ddb8914bf9bf64ae95b58b", + ] + }, + "extrinsicsRoot": "0xdec7259ef64ad7fe645b11a2814ea1cdb56783399b7ae14f54838186c7ef7659", + "number": "0x31ce99", + "parentHash": "0x056fb97cc61bb3892f5c1f58574cd8a489ccfb7681079137b7bb2f7aaaaeef35", + "stateRoot": "0xd97a981b0fe489518abb7bd1d7375cb6f4505ea95e0a8c97e7e08013b19bc32c", + }, + } + }, + "rpc_methods": { + "[]": { + "jsonrpc": "2.0", + "result": { + "methods": [ + "account_nextIndex", + "archive_unstable_body", + "archive_unstable_call", + "archive_unstable_finalizedHeight", + "archive_unstable_genesisHash", + "archive_unstable_hashByHeight", + "archive_unstable_header", + "archive_unstable_storage", + "author_hasKey", + "author_hasSessionKeys", + "author_insertKey", + "author_pendingExtrinsics", + "author_removeExtrinsic", + "author_rotateKeys", + "author_submitAndWatchExtrinsic", + "author_submitExtrinsic", + "author_unwatchExtrinsic", + "chainHead_v1_body", + "chainHead_v1_call", + "chainHead_v1_continue", + "chainHead_v1_follow", + "chainHead_v1_header", + "chainHead_v1_stopOperation", + "chainHead_v1_storage", + "chainHead_v1_unfollow", + "chainHead_v1_unpin", + "chainSpec_v1_chainName", + "chainSpec_v1_genesisHash", + "chainSpec_v1_properties", + "chain_getBlock", + "chain_getBlockHash", + "chain_getFinalisedHead", + "chain_getFinalizedHead", + "chain_getHead", + "chain_getHeader", + "chain_getRuntimeVersion", + "chain_subscribeAllHeads", + "chain_subscribeFinalisedHeads", + "chain_subscribeFinalizedHeads", + "chain_subscribeNewHead", + "chain_subscribeNewHeads", + "chain_subscribeRuntimeVersion", + "chain_unsubscribeAllHeads", + "chain_unsubscribeFinalisedHeads", + "chain_unsubscribeFinalizedHeads", + "chain_unsubscribeNewHead", + "chain_unsubscribeNewHeads", + "chain_unsubscribeRuntimeVersion", + "childstate_getKeys", + "childstate_getKeysPaged", + "childstate_getKeysPagedAt", + "childstate_getStorage", + "childstate_getStorageEntries", + "childstate_getStorageHash", + "childstate_getStorageSize", + "debug_getBadBlocks", + "debug_getRawBlock", + "debug_getRawHeader", + "debug_getRawReceipts", + "debug_getRawTransaction", + "delegateInfo_getDelegate", + "delegateInfo_getDelegated", + "delegateInfo_getDelegates", + "eth_accounts", + "eth_blockNumber", + "eth_call", + "eth_chainId", + "eth_coinbase", + "eth_estimateGas", + "eth_feeHistory", + "eth_gasPrice", + "eth_getBalance", + "eth_getBlockByHash", + "eth_getBlockByNumber", + "eth_getBlockReceipts", + "eth_getBlockTransactionCountByHash", + "eth_getBlockTransactionCountByNumber", + "eth_getCode", + "eth_getFilterChanges", + "eth_getFilterLogs", + "eth_getLogs", + "eth_getStorageAt", + "eth_getTransactionByBlockHashAndIndex", + "eth_getTransactionByBlockNumberAndIndex", + "eth_getTransactionByHash", + "eth_getTransactionCount", + "eth_getTransactionReceipt", + "eth_getUncleByBlockHashAndIndex", + "eth_getUncleByBlockNumberAndIndex", + "eth_getUncleCountByBlockHash", + "eth_getUncleCountByBlockNumber", + "eth_getWork", + "eth_hashrate", + "eth_maxPriorityFeePerGas", + "eth_mining", + "eth_newBlockFilter", + "eth_newFilter", + "eth_newPendingTransactionFilter", + "eth_protocolVersion", + "eth_sendRawTransaction", + "eth_sendTransaction", + "eth_submitHashrate", + "eth_submitWork", + "eth_subscribe", + "eth_syncing", + "eth_uninstallFilter", + "eth_unsubscribe", + "net_listening", + "net_peerCount", + "net_version", + "neuronInfo_getNeuron", + "neuronInfo_getNeuronLite", + "neuronInfo_getNeurons", + "neuronInfo_getNeuronsLite", + "offchain_localStorageGet", + "offchain_localStorageSet", + "payment_queryFeeDetails", + "payment_queryInfo", + "rpc_methods", + "state_call", + "state_callAt", + "state_getChildReadProof", + "state_getKeys", + "state_getKeysPaged", + "state_getKeysPagedAt", + "state_getMetadata", + "state_getPairs", + "state_getReadProof", + "state_getRuntimeVersion", + "state_getStorage", + "state_getStorageAt", + "state_getStorageHash", + "state_getStorageHashAt", + "state_getStorageSize", + "state_getStorageSizeAt", + "state_queryStorage", + "state_queryStorageAt", + "state_subscribeRuntimeVersion", + "state_subscribeStorage", + "state_traceBlock", + "state_unsubscribeRuntimeVersion", + "state_unsubscribeStorage", + "subnetInfo_getLockCost", + "subnetInfo_getSubnetHyperparams", + "subnetInfo_getSubnetInfo", + "subnetInfo_getSubnetInfo_v2", + "subnetInfo_getSubnetsInf_v2", + "subnetInfo_getSubnetsInfo", + "subscribe_newHead", + "system_accountNextIndex", + "system_addLogFilter", + "system_addReservedPeer", + "system_chain", + "system_chainType", + "system_dryRun", + "system_dryRunAt", + "system_health", + "system_localListenAddresses", + "system_localPeerId", + "system_name", + "system_nodeRoles", + "system_peers", + "system_properties", + "system_removeReservedPeer", + "system_reservedPeers", + "system_resetLogFilter", + "system_syncState", + "system_unstable_networkState", + "system_version", + "transactionWatch_v1_submitAndWatch", + "transactionWatch_v1_unwatch", + "transaction_v1_broadcast", + "transaction_v1_stop", + "unsubscribe_newHead", + "web3_clientVersion", + "web3_sha3", + ] + }, + } + }, + "state_getRuntimeVersion": { + '["0x056fb97cc61bb3892f5c1f58574cd8a489ccfb7681079137b7bb2f7aaaaeef35"]': { + "jsonrpc": "2.0", + "result": { + "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", 5], + ["0xe65b00e46cedd0aa", 2], + ["0x42e62be4a39e5b60", 1], + ["0x806df4ccaa9ed485", 1], + ["0x8375104b299b74c5", 1], + ["0x5d1fbfbe852f2807", 1], + ["0xc6886e2f8e598b0a", 1], + ], + "authoringVersion": 1, + "implName": "node-subtensor", + "implVersion": 1, + "specName": "node-subtensor", + "specVersion": 208, + "stateVersion": 1, + "transactionVersion": 1, + }, + } + }, + "state_getStorageAt": { + '["0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a431700", null]': { + "jsonrpc": "2.0", + "result": "0x01", + } + }, + "system_chain": {"[]": {"jsonrpc": "2.0", "result": "Bittensor"}}, + }, + "subnetwork_n": { + "chain_getHead": { + "[]": { + "jsonrpc": "2.0", + "result": "0x9d58a80049c3eaabf68f6bc83b1dd3f36bc3dcc7d75be17ee53dfc678e4ee111", + } + }, + "chain_getHeader": { + '["0x9d58a80049c3eaabf68f6bc83b1dd3f36bc3dcc7d75be17ee53dfc678e4ee111"]': { + "jsonrpc": "2.0", + "result": { + "digest": { + "logs": [ + "0x066175726120a0489a0800000000", + "0x0466726f6e8801b2557a32e7b78d386e67769c083405a148fe57d80528d9aca8eb9ddbbfac288800", + "0x05617572610101b8a3e510b65d00ebc479d3102632250e7354a619d7719757df6b0666c1eaa0039debd861b6ae79fcbc4f82b87d5efbc5ca089e318918e5274d0380782c91188b", + ] + }, + "extrinsicsRoot": "0x226f5c8e7d52bebaa5fb56a9a9f8344667533c2f9edac2eed9c91c64a9cc653c", + "number": "0x31ce93", + "parentHash": "0x9259b771b0f59b1ed8f38f604ab4f4a3e63dc0988754e664be2d40d58970958c", + "stateRoot": "0x8d8552df3fa7fb8569216cc69516dd3067af8d5bad60cf363c1337719bd31a12", + }, + } + }, + "rpc_methods": { + "[]": { + "jsonrpc": "2.0", + "result": { + "methods": [ + "account_nextIndex", + "archive_unstable_body", + "archive_unstable_call", + "archive_unstable_finalizedHeight", + "archive_unstable_genesisHash", + "archive_unstable_hashByHeight", + "archive_unstable_header", + "archive_unstable_storage", + "author_hasKey", + "author_hasSessionKeys", + "author_insertKey", + "author_pendingExtrinsics", + "author_removeExtrinsic", + "author_rotateKeys", + "author_submitAndWatchExtrinsic", + "author_submitExtrinsic", + "author_unwatchExtrinsic", + "chainHead_v1_body", + "chainHead_v1_call", + "chainHead_v1_continue", + "chainHead_v1_follow", + "chainHead_v1_header", + "chainHead_v1_stopOperation", + "chainHead_v1_storage", + "chainHead_v1_unfollow", + "chainHead_v1_unpin", + "chainSpec_v1_chainName", + "chainSpec_v1_genesisHash", + "chainSpec_v1_properties", + "chain_getBlock", + "chain_getBlockHash", + "chain_getFinalisedHead", + "chain_getFinalizedHead", + "chain_getHead", + "chain_getHeader", + "chain_getRuntimeVersion", + "chain_subscribeAllHeads", + "chain_subscribeFinalisedHeads", + "chain_subscribeFinalizedHeads", + "chain_subscribeNewHead", + "chain_subscribeNewHeads", + "chain_subscribeRuntimeVersion", + "chain_unsubscribeAllHeads", + "chain_unsubscribeFinalisedHeads", + "chain_unsubscribeFinalizedHeads", + "chain_unsubscribeNewHead", + "chain_unsubscribeNewHeads", + "chain_unsubscribeRuntimeVersion", + "childstate_getKeys", + "childstate_getKeysPaged", + "childstate_getKeysPagedAt", + "childstate_getStorage", + "childstate_getStorageEntries", + "childstate_getStorageHash", + "childstate_getStorageSize", + "debug_getBadBlocks", + "debug_getRawBlock", + "debug_getRawHeader", + "debug_getRawReceipts", + "debug_getRawTransaction", + "delegateInfo_getDelegate", + "delegateInfo_getDelegated", + "delegateInfo_getDelegates", + "eth_accounts", + "eth_blockNumber", + "eth_call", + "eth_chainId", + "eth_coinbase", + "eth_estimateGas", + "eth_feeHistory", + "eth_gasPrice", + "eth_getBalance", + "eth_getBlockByHash", + "eth_getBlockByNumber", + "eth_getBlockReceipts", + "eth_getBlockTransactionCountByHash", + "eth_getBlockTransactionCountByNumber", + "eth_getCode", + "eth_getFilterChanges", + "eth_getFilterLogs", + "eth_getLogs", + "eth_getStorageAt", + "eth_getTransactionByBlockHashAndIndex", + "eth_getTransactionByBlockNumberAndIndex", + "eth_getTransactionByHash", + "eth_getTransactionCount", + "eth_getTransactionReceipt", + "eth_getUncleByBlockHashAndIndex", + "eth_getUncleByBlockNumberAndIndex", + "eth_getUncleCountByBlockHash", + "eth_getUncleCountByBlockNumber", + "eth_getWork", + "eth_hashrate", + "eth_maxPriorityFeePerGas", + "eth_mining", + "eth_newBlockFilter", + "eth_newFilter", + "eth_newPendingTransactionFilter", + "eth_protocolVersion", + "eth_sendRawTransaction", + "eth_sendTransaction", + "eth_submitHashrate", + "eth_submitWork", + "eth_subscribe", + "eth_syncing", + "eth_uninstallFilter", + "eth_unsubscribe", + "net_listening", + "net_peerCount", + "net_version", + "neuronInfo_getNeuron", + "neuronInfo_getNeuronLite", + "neuronInfo_getNeurons", + "neuronInfo_getNeuronsLite", + "offchain_localStorageGet", + "offchain_localStorageSet", + "payment_queryFeeDetails", + "payment_queryInfo", + "rpc_methods", + "state_call", + "state_callAt", + "state_getChildReadProof", + "state_getKeys", + "state_getKeysPaged", + "state_getKeysPagedAt", + "state_getMetadata", + "state_getPairs", + "state_getReadProof", + "state_getRuntimeVersion", + "state_getStorage", + "state_getStorageAt", + "state_getStorageHash", + "state_getStorageHashAt", + "state_getStorageSize", + "state_getStorageSizeAt", + "state_queryStorage", + "state_queryStorageAt", + "state_subscribeRuntimeVersion", + "state_subscribeStorage", + "state_traceBlock", + "state_unsubscribeRuntimeVersion", + "state_unsubscribeStorage", + "subnetInfo_getLockCost", + "subnetInfo_getSubnetHyperparams", + "subnetInfo_getSubnetInfo", + "subnetInfo_getSubnetInfo_v2", + "subnetInfo_getSubnetsInf_v2", + "subnetInfo_getSubnetsInfo", + "subscribe_newHead", + "system_accountNextIndex", + "system_addLogFilter", + "system_addReservedPeer", + "system_chain", + "system_chainType", + "system_dryRun", + "system_dryRunAt", + "system_health", + "system_localListenAddresses", + "system_localPeerId", + "system_name", + "system_nodeRoles", + "system_peers", + "system_properties", + "system_removeReservedPeer", + "system_reservedPeers", + "system_resetLogFilter", + "system_syncState", + "system_unstable_networkState", + "system_version", + "transactionWatch_v1_submitAndWatch", + "transactionWatch_v1_unwatch", + "transaction_v1_broadcast", + "transaction_v1_stop", + "unsubscribe_newHead", + "web3_clientVersion", + "web3_sha3", + ] + }, + } + }, + "state_getRuntimeVersion": { + '["0x9259b771b0f59b1ed8f38f604ab4f4a3e63dc0988754e664be2d40d58970958c"]': { + "jsonrpc": "2.0", + "result": { + "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", 5], + ["0xe65b00e46cedd0aa", 2], + ["0x42e62be4a39e5b60", 1], + ["0x806df4ccaa9ed485", 1], + ["0x8375104b299b74c5", 1], + ["0x5d1fbfbe852f2807", 1], + ["0xc6886e2f8e598b0a", 1], + ], + "authoringVersion": 1, + "implName": "node-subtensor", + "implVersion": 1, + "specName": "node-subtensor", + "specVersion": 208, + "stateVersion": 1, + "transactionVersion": 1, + }, + } + }, + "state_getStorageAt": { + '["0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a430100", null]': { + "jsonrpc": "2.0", + "result": "0x01", + }, + '["0x658faa385070e074c85bf6b568cf0555a1048e9d244171852dfe8db314dc68ca0100", null]': { + "jsonrpc": "2.0", + "result": "0x5e00", + }, + }, + "system_chain": {"[]": {"jsonrpc": "2.0", "result": "Bittensor"}}, + }, + "tempo": { + "chain_getHead": { + "[]": { + "jsonrpc": "2.0", + "result": "0x03a008636efbe7706f79f2c7d7d4e85ff26a3e67922e5fefab71e7f6d9dab43d", + } + }, + "chain_getHeader": { + '["0x03a008636efbe7706f79f2c7d7d4e85ff26a3e67922e5fefab71e7f6d9dab43d"]': { + "jsonrpc": "2.0", + "result": { + "digest": { + "logs": [ + "0x066175726120a3489a0800000000", + "0x0466726f6e88016fc9c1d390ad4593c6f2722380d730447c348aae252de386d6d3c23b40ec2f6000", + "0x05617572610101bef9823e84987376a43e19ada6b2ff71671678ea2d9f6261e113bb74328fb77c0973b1b20e558d90e781bd3b0011a9c641563ff75f8848eb4d78a19fbd20d286", + ] + }, + "extrinsicsRoot": "0xfce4b69a3d9dc862c691b325955ca964ba673cc36d7489c66402ba6d45fdf815", + "number": "0x31ce96", + "parentHash": "0x8d3cb34e83a45c0d2a2896bbe5fd34e108d9d82b7261a4eaf1c4b585c17a4689", + "stateRoot": "0x4f9450db42aae09613ddea5e5187b5b07534c0a84ddbf813464693295a994a3d", + }, + } + }, + "rpc_methods": { + "[]": { + "jsonrpc": "2.0", + "result": { + "methods": [ + "account_nextIndex", + "archive_unstable_body", + "archive_unstable_call", + "archive_unstable_finalizedHeight", + "archive_unstable_genesisHash", + "archive_unstable_hashByHeight", + "archive_unstable_header", + "archive_unstable_storage", + "author_hasKey", + "author_hasSessionKeys", + "author_insertKey", + "author_pendingExtrinsics", + "author_removeExtrinsic", + "author_rotateKeys", + "author_submitAndWatchExtrinsic", + "author_submitExtrinsic", + "author_unwatchExtrinsic", + "chainHead_v1_body", + "chainHead_v1_call", + "chainHead_v1_continue", + "chainHead_v1_follow", + "chainHead_v1_header", + "chainHead_v1_stopOperation", + "chainHead_v1_storage", + "chainHead_v1_unfollow", + "chainHead_v1_unpin", + "chainSpec_v1_chainName", + "chainSpec_v1_genesisHash", + "chainSpec_v1_properties", + "chain_getBlock", + "chain_getBlockHash", + "chain_getFinalisedHead", + "chain_getFinalizedHead", + "chain_getHead", + "chain_getHeader", + "chain_getRuntimeVersion", + "chain_subscribeAllHeads", + "chain_subscribeFinalisedHeads", + "chain_subscribeFinalizedHeads", + "chain_subscribeNewHead", + "chain_subscribeNewHeads", + "chain_subscribeRuntimeVersion", + "chain_unsubscribeAllHeads", + "chain_unsubscribeFinalisedHeads", + "chain_unsubscribeFinalizedHeads", + "chain_unsubscribeNewHead", + "chain_unsubscribeNewHeads", + "chain_unsubscribeRuntimeVersion", + "childstate_getKeys", + "childstate_getKeysPaged", + "childstate_getKeysPagedAt", + "childstate_getStorage", + "childstate_getStorageEntries", + "childstate_getStorageHash", + "childstate_getStorageSize", + "debug_getBadBlocks", + "debug_getRawBlock", + "debug_getRawHeader", + "debug_getRawReceipts", + "debug_getRawTransaction", + "delegateInfo_getDelegate", + "delegateInfo_getDelegated", + "delegateInfo_getDelegates", + "eth_accounts", + "eth_blockNumber", + "eth_call", + "eth_chainId", + "eth_coinbase", + "eth_estimateGas", + "eth_feeHistory", + "eth_gasPrice", + "eth_getBalance", + "eth_getBlockByHash", + "eth_getBlockByNumber", + "eth_getBlockReceipts", + "eth_getBlockTransactionCountByHash", + "eth_getBlockTransactionCountByNumber", + "eth_getCode", + "eth_getFilterChanges", + "eth_getFilterLogs", + "eth_getLogs", + "eth_getStorageAt", + "eth_getTransactionByBlockHashAndIndex", + "eth_getTransactionByBlockNumberAndIndex", + "eth_getTransactionByHash", + "eth_getTransactionCount", + "eth_getTransactionReceipt", + "eth_getUncleByBlockHashAndIndex", + "eth_getUncleByBlockNumberAndIndex", + "eth_getUncleCountByBlockHash", + "eth_getUncleCountByBlockNumber", + "eth_getWork", + "eth_hashrate", + "eth_maxPriorityFeePerGas", + "eth_mining", + "eth_newBlockFilter", + "eth_newFilter", + "eth_newPendingTransactionFilter", + "eth_protocolVersion", + "eth_sendRawTransaction", + "eth_sendTransaction", + "eth_submitHashrate", + "eth_submitWork", + "eth_subscribe", + "eth_syncing", + "eth_uninstallFilter", + "eth_unsubscribe", + "net_listening", + "net_peerCount", + "net_version", + "neuronInfo_getNeuron", + "neuronInfo_getNeuronLite", + "neuronInfo_getNeurons", + "neuronInfo_getNeuronsLite", + "offchain_localStorageGet", + "offchain_localStorageSet", + "payment_queryFeeDetails", + "payment_queryInfo", + "rpc_methods", + "state_call", + "state_callAt", + "state_getChildReadProof", + "state_getKeys", + "state_getKeysPaged", + "state_getKeysPagedAt", + "state_getMetadata", + "state_getPairs", + "state_getReadProof", + "state_getRuntimeVersion", + "state_getStorage", + "state_getStorageAt", + "state_getStorageHash", + "state_getStorageHashAt", + "state_getStorageSize", + "state_getStorageSizeAt", + "state_queryStorage", + "state_queryStorageAt", + "state_subscribeRuntimeVersion", + "state_subscribeStorage", + "state_traceBlock", + "state_unsubscribeRuntimeVersion", + "state_unsubscribeStorage", + "subnetInfo_getLockCost", + "subnetInfo_getSubnetHyperparams", + "subnetInfo_getSubnetInfo", + "subnetInfo_getSubnetInfo_v2", + "subnetInfo_getSubnetsInf_v2", + "subnetInfo_getSubnetsInfo", + "subscribe_newHead", + "system_accountNextIndex", + "system_addLogFilter", + "system_addReservedPeer", + "system_chain", + "system_chainType", + "system_dryRun", + "system_dryRunAt", + "system_health", + "system_localListenAddresses", + "system_localPeerId", + "system_name", + "system_nodeRoles", + "system_peers", + "system_properties", + "system_removeReservedPeer", + "system_reservedPeers", + "system_resetLogFilter", + "system_syncState", + "system_unstable_networkState", + "system_version", + "transactionWatch_v1_submitAndWatch", + "transactionWatch_v1_unwatch", + "transaction_v1_broadcast", + "transaction_v1_stop", + "unsubscribe_newHead", + "web3_clientVersion", + "web3_sha3", + ] + }, + } + }, + "state_getRuntimeVersion": { + '["0x8d3cb34e83a45c0d2a2896bbe5fd34e108d9d82b7261a4eaf1c4b585c17a4689"]': { + "jsonrpc": "2.0", + "result": { + "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", 5], + ["0xe65b00e46cedd0aa", 2], + ["0x42e62be4a39e5b60", 1], + ["0x806df4ccaa9ed485", 1], + ["0x8375104b299b74c5", 1], + ["0x5d1fbfbe852f2807", 1], + ["0xc6886e2f8e598b0a", 1], + ], + "authoringVersion": 1, + "implName": "node-subtensor", + "implVersion": 1, + "specName": "node-subtensor", + "specVersion": 208, + "stateVersion": 1, + "transactionVersion": 1, + }, + } + }, + "state_getStorageAt": { + '["0x658faa385070e074c85bf6b568cf05550e30450fc4d507a846032a7fa65d9a431700", null]': { + "jsonrpc": "2.0", + "result": "0x01", + }, + '["0x658faa385070e074c85bf6b568cf05557641384bb339f3758acddfd7053d33171700", "0x03a008636efbe7706f79f2c7d7d4e85ff26a3e67922e5fefab71e7f6d9dab43d"]': { + "jsonrpc": "2.0", + "result": "0xe803", + }, + }, + "system_chain": {"[]": {"jsonrpc": "2.0", "result": "Bittensor"}}, + }, + "weights": { + "chain_getHead": { + "[]": { + "jsonrpc": "2.0", + "result": "0x1b3f85dc8c83cada6d1782c955973a509a37c62af3d6ba29f5fd816aee37eb3b", + } + }, + "chain_getHeader": { + '["0x1b3f85dc8c83cada6d1782c955973a509a37c62af3d6ba29f5fd816aee37eb3b"]': { + "jsonrpc": "2.0", + "result": { + "digest": { + "logs": [ + "0x066175726120ab489a0800000000", + "0x0466726f6e88015b559367fb27665be180a32109a44b153dd14cafb22eb908ac9e79a308916c9a00", + "0x056175726101012030b4a878800704102801d12f2dfe4b3f66bd8c3fbe236e4881ed1b9bdbaf6c8eb6200f9db52945187c3c1f46d8b4c08c1b50e2002d94594e0c751069ca7d8d", + ] + }, + "extrinsicsRoot": "0xf7eaa2bb07e7650b27ee819ee07c987207ad40220f250e4eebb1178e19734242", + "number": "0x31ce9e", + "parentHash": "0xd867f39bb9e2a626945183af01065b61d0695d435a765bfd0ce5f821fbed8f82", + "stateRoot": "0x3096ed3b5859801b59e2fb348ef560e87f473a799dea9be12982b969fdd1dfaf", + }, + } + }, + "rpc_methods": { + "[]": { + "jsonrpc": "2.0", + "result": { + "methods": [ + "account_nextIndex", + "archive_unstable_body", + "archive_unstable_call", + "archive_unstable_finalizedHeight", + "archive_unstable_genesisHash", + "archive_unstable_hashByHeight", + "archive_unstable_header", + "archive_unstable_storage", + "author_hasKey", + "author_hasSessionKeys", + "author_insertKey", + "author_pendingExtrinsics", + "author_removeExtrinsic", + "author_rotateKeys", + "author_submitAndWatchExtrinsic", + "author_submitExtrinsic", + "author_unwatchExtrinsic", + "chainHead_v1_body", + "chainHead_v1_call", + "chainHead_v1_continue", + "chainHead_v1_follow", + "chainHead_v1_header", + "chainHead_v1_stopOperation", + "chainHead_v1_storage", + "chainHead_v1_unfollow", + "chainHead_v1_unpin", + "chainSpec_v1_chainName", + "chainSpec_v1_genesisHash", + "chainSpec_v1_properties", + "chain_getBlock", + "chain_getBlockHash", + "chain_getFinalisedHead", + "chain_getFinalizedHead", + "chain_getHead", + "chain_getHeader", + "chain_getRuntimeVersion", + "chain_subscribeAllHeads", + "chain_subscribeFinalisedHeads", + "chain_subscribeFinalizedHeads", + "chain_subscribeNewHead", + "chain_subscribeNewHeads", + "chain_subscribeRuntimeVersion", + "chain_unsubscribeAllHeads", + "chain_unsubscribeFinalisedHeads", + "chain_unsubscribeFinalizedHeads", + "chain_unsubscribeNewHead", + "chain_unsubscribeNewHeads", + "chain_unsubscribeRuntimeVersion", + "childstate_getKeys", + "childstate_getKeysPaged", + "childstate_getKeysPagedAt", + "childstate_getStorage", + "childstate_getStorageEntries", + "childstate_getStorageHash", + "childstate_getStorageSize", + "debug_getBadBlocks", + "debug_getRawBlock", + "debug_getRawHeader", + "debug_getRawReceipts", + "debug_getRawTransaction", + "delegateInfo_getDelegate", + "delegateInfo_getDelegated", + "delegateInfo_getDelegates", + "eth_accounts", + "eth_blockNumber", + "eth_call", + "eth_chainId", + "eth_coinbase", + "eth_estimateGas", + "eth_feeHistory", + "eth_gasPrice", + "eth_getBalance", + "eth_getBlockByHash", + "eth_getBlockByNumber", + "eth_getBlockReceipts", + "eth_getBlockTransactionCountByHash", + "eth_getBlockTransactionCountByNumber", + "eth_getCode", + "eth_getFilterChanges", + "eth_getFilterLogs", + "eth_getLogs", + "eth_getStorageAt", + "eth_getTransactionByBlockHashAndIndex", + "eth_getTransactionByBlockNumberAndIndex", + "eth_getTransactionByHash", + "eth_getTransactionCount", + "eth_getTransactionReceipt", + "eth_getUncleByBlockHashAndIndex", + "eth_getUncleByBlockNumberAndIndex", + "eth_getUncleCountByBlockHash", + "eth_getUncleCountByBlockNumber", + "eth_getWork", + "eth_hashrate", + "eth_maxPriorityFeePerGas", + "eth_mining", + "eth_newBlockFilter", + "eth_newFilter", + "eth_newPendingTransactionFilter", + "eth_protocolVersion", + "eth_sendRawTransaction", + "eth_sendTransaction", + "eth_submitHashrate", + "eth_submitWork", + "eth_subscribe", + "eth_syncing", + "eth_uninstallFilter", + "eth_unsubscribe", + "net_listening", + "net_peerCount", + "net_version", + "neuronInfo_getNeuron", + "neuronInfo_getNeuronLite", + "neuronInfo_getNeurons", + "neuronInfo_getNeuronsLite", + "offchain_localStorageGet", + "offchain_localStorageSet", + "payment_queryFeeDetails", + "payment_queryInfo", + "rpc_methods", + "state_call", + "state_callAt", + "state_getChildReadProof", + "state_getKeys", + "state_getKeysPaged", + "state_getKeysPagedAt", + "state_getMetadata", + "state_getPairs", + "state_getReadProof", + "state_getRuntimeVersion", + "state_getStorage", + "state_getStorageAt", + "state_getStorageHash", + "state_getStorageHashAt", + "state_getStorageSize", + "state_getStorageSizeAt", + "state_queryStorage", + "state_queryStorageAt", + "state_subscribeRuntimeVersion", + "state_subscribeStorage", + "state_traceBlock", + "state_unsubscribeRuntimeVersion", + "state_unsubscribeStorage", + "subnetInfo_getLockCost", + "subnetInfo_getSubnetHyperparams", + "subnetInfo_getSubnetInfo", + "subnetInfo_getSubnetInfo_v2", + "subnetInfo_getSubnetsInf_v2", + "subnetInfo_getSubnetsInfo", + "subscribe_newHead", + "system_accountNextIndex", + "system_addLogFilter", + "system_addReservedPeer", + "system_chain", + "system_chainType", + "system_dryRun", + "system_dryRunAt", + "system_health", + "system_localListenAddresses", + "system_localPeerId", + "system_name", + "system_nodeRoles", + "system_peers", + "system_properties", + "system_removeReservedPeer", + "system_reservedPeers", + "system_resetLogFilter", + "system_syncState", + "system_unstable_networkState", + "system_version", + "transactionWatch_v1_submitAndWatch", + "transactionWatch_v1_unwatch", + "transaction_v1_broadcast", + "transaction_v1_stop", + "unsubscribe_newHead", + "web3_clientVersion", + "web3_sha3", + ] + }, + } + }, + "state_getKeysPaged": { + '["0x658faa385070e074c85bf6b568cf0555a1e7036061f484bc3048a64b64e35ec41700", 100, "0x658faa385070e074c85bf6b568cf0555a1e7036061f484bc3048a64b64e35ec41700", "0x1b3f85dc8c83cada6d1782c955973a509a37c62af3d6ba29f5fd816aee37eb3b"]': { + "jsonrpc": "2.0", + "result": [ + "0x658faa385070e074c85bf6b568cf0555a1e7036061f484bc3048a64b64e35ec417000a00" + ], + }, + '["0x658faa385070e074c85bf6b568cf0555a1e7036061f484bc3048a64b64e35ec41700", 100, "0x658faa385070e074c85bf6b568cf0555a1e7036061f484bc3048a64b64e35ec417000a00", "0x1b3f85dc8c83cada6d1782c955973a509a37c62af3d6ba29f5fd816aee37eb3b"]': { + "jsonrpc": "2.0", + "result": [], + }, + }, + "state_getRuntimeVersion": { + '["0xd867f39bb9e2a626945183af01065b61d0695d435a765bfd0ce5f821fbed8f82"]': { + "jsonrpc": "2.0", + "result": { + "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", 5], + ["0xe65b00e46cedd0aa", 2], + ["0x42e62be4a39e5b60", 1], + ["0x806df4ccaa9ed485", 1], + ["0x8375104b299b74c5", 1], + ["0x5d1fbfbe852f2807", 1], + ["0xc6886e2f8e598b0a", 1], + ], + "authoringVersion": 1, + "implName": "node-subtensor", + "implVersion": 1, + "specName": "node-subtensor", + "specVersion": 208, + "stateVersion": 1, + "transactionVersion": 1, + }, + } + }, + "state_queryStorageAt": { + '[["0x658faa385070e074c85bf6b568cf0555a1e7036061f484bc3048a64b64e35ec417000a00"], "0x1b3f85dc8c83cada6d1782c955973a509a37c62af3d6ba29f5fd816aee37eb3b"]': { + "jsonrpc": "2.0", + "result": [ + { + "block": "0x1b3f85dc8c83cada6d1782c955973a509a37c62af3d6ba29f5fd816aee37eb3b", + "changes": [ + [ + "0x658faa385070e074c85bf6b568cf0555a1e7036061f484bc3048a64b64e35ec417000a00", + "0x040a00ffff", + ] + ], + } + ], + } + }, + "system_chain": {"[]": {"jsonrpc": "2.0", "result": "Bittensor"}}, + }, + "retry_archive": { + "system_chain": {"[]": {"jsonrpc": "2.0", "result": "Bittensor"}}, + "chain_getHead": { + "[]": { + "jsonrpc": "2.0", + "result": "0xae6bd3bf8cc2a7660a315510f72a7e8c8dc31ad58c8d847e86682ee07c6e8938", + } + }, + "chain_getHeader": { + '["0x0f28c25f01cc4b11bb1454c323bd3d04658e4e64b69fcd21ffa01bac0ae47b19"]': { + "jsonrpc": "2.0", + "result": { + "parentHash": "0x3c1304811d6b0f5741c0ce9fcd46416c3fe151561463134b0fa9b68f1bd0188d", + "number": "0x57c8bb", + "stateRoot": "0x48b804f020e871af4eae34fb9d1fae077ff988016e65fafc9e3acbcdc8cfc8d9", + "extrinsicsRoot": "0x9b28450ef2ca7730b29e104d85e65e23cf021d405fb0fb29226dd04b89d36182", + "digest": { + "logs": [ + "0x06617572612023b8b00800000000", + "0x0466726f6e8801446b1f0502f0e66079329d4718fb2f5cd5660a74a1bc0c7556c9ecbd92b54b3700", + "0x05617572610101a8461545e8f4947dd540f2f2131fdf29117e471a9c46970a408ef9684dbf495cdaebeaf8a42efcf0a57fdf75ddf807fb1f4c21fb1fb74b6715cf59a23f24d98e", + ] + }, + }, + }, + "[null]": { + "jsonrpc": "2.0", + "result": { + "parentHash": "0x3c1304811d6b0f5741c0ce9fcd46416c3fe151561463134b0fa9b68f1bd0188d", + "number": "0x57c8bb", + "stateRoot": "0x48b804f020e871af4eae34fb9d1fae077ff988016e65fafc9e3acbcdc8cfc8d9", + "extrinsicsRoot": "0x9b28450ef2ca7730b29e104d85e65e23cf021d405fb0fb29226dd04b89d36182", + "digest": { + "logs": [ + "0x06617572612023b8b00800000000", + "0x0466726f6e8801446b1f0502f0e66079329d4718fb2f5cd5660a74a1bc0c7556c9ecbd92b54b3700", + "0x05617572610101a8461545e8f4947dd540f2f2131fdf29117e471a9c46970a408ef9684dbf495cdaebeaf8a42efcf0a57fdf75ddf807fb1f4c21fb1fb74b6715cf59a23f24d98e", + ] + }, + }, + }, + '["0x0ba73c8d5a0b78f7336a3e394a2a1fa7b7dec0924937b1dfb05a50ac3dd5cfc1"]': { + "jsonrpc": "2.0", + "result": { + "parentHash": "0x6c0da8a9dd5994d374d43210dc401f000b2fe02541bc2e7cd54719655d751a18", + "number": "0x57c4d3", + "stateRoot": "0xc80611aa0e605bc2d0446479dcbce6bee0e1524d07b0a9c6293a190d6a500fef", + "extrinsicsRoot": "0x9ff58560a13b06d4a61af01ca9f0dd2a96cf5bfa6bbc2208fc7e6f763bad5590", + "digest": { + "logs": [ + "0x0661757261203bb4b00800000000", + "0x0466726f6e880102e3f23c7b0347be401daa27b4bd35d09948dc1450bec025807957f499347c5800", + "0x05617572610101bcc856c718c722f327d3fbc3f94c34d97e0bfc794531e94f5590b44b3516c7598ca0ec488b81f9fbf5a9bbb5d18663bd69ca032d1c7134681d9c112387612a87", + ] + }, + }, + }, + '["0xae6bd3bf8cc2a7660a315510f72a7e8c8dc31ad58c8d847e86682ee07c6e8938"]': { + "jsonrpc": "2.0", + "result": { + "parentHash": "0x0f28c25f01cc4b11bb1454c323bd3d04658e4e64b69fcd21ffa01bac0ae47b19", + "number": "0x57c8bc", + "stateRoot": "0x87f0259d14e63525a311f4afabd7273519ac2e98e2dc70cf17292b615889b714", + "extrinsicsRoot": "0x24c53c5248e9e822fb29c381cee462e5f830bba62097d58607dead8da7f3b64b", + "digest": { + "logs": [ + "0x06617572612024b8b00800000000", + "0x0466726f6e88016e91817304fa73ea8595ba3bd2e26fb838c7495ae83501bb266013b2307170f200", + "0x0561757261010152b29d8299665fd87748987347b6b91d206c04a5939ddb543e4776adcfb0ff47196fdaec1a0e2a2225e0485f1fa91b5c4421ff54944df488dcf35ecbc7a57087", + ] + }, + }, + }, + }, + "state_getRuntimeVersion": { + '["0x3c1304811d6b0f5741c0ce9fcd46416c3fe151561463134b0fa9b68f1bd0188d"]': { + "jsonrpc": "2.0", + "result": { + "specName": "node-subtensor", + "implName": "node-subtensor", + "authoringVersion": 1, + "specVersion": 274, + "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", 5], + ["0xe65b00e46cedd0aa", 2], + ["0x42e62be4a39e5b60", 1], + ["0x806df4ccaa9ed485", 1], + ["0x8375104b299b74c5", 1], + ["0x5d1fbfbe852f2807", 1], + ["0xc6886e2f8e598b0a", 1], + ], + "transactionVersion": 1, + "stateVersion": 1, + }, + }, + '["0x6c0da8a9dd5994d374d43210dc401f000b2fe02541bc2e7cd54719655d751a18"]': cycle( + ( + { + "jsonrpc": "2.0", + "id": "oj19", + "error": { + "code": 4003, + "message": "Client error: Api called for an unknown Block: State already discarded for 0x6c0da8a9dd5994d374d43210dc401f000b2fe02541bc2e7cd54719655d751a18", + }, + }, + { + "jsonrpc": "2.0", + "result": { + "specName": "node-subtensor", + "implName": "node-subtensor", + "authoringVersion": 1, + "specVersion": 273, + "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", 5], + ["0xe65b00e46cedd0aa", 2], + ["0x42e62be4a39e5b60", 1], + ["0x806df4ccaa9ed485", 1], + ["0x8375104b299b74c5", 1], + ["0x5d1fbfbe852f2807", 1], + ["0xc6886e2f8e598b0a", 1], + ], + "transactionVersion": 1, + "stateVersion": 1, + }, + }, + ) + ), + '["0x0f28c25f01cc4b11bb1454c323bd3d04658e4e64b69fcd21ffa01bac0ae47b19"]': { + "jsonrpc": "2.0", + "result": { + "specName": "node-subtensor", + "implName": "node-subtensor", + "authoringVersion": 1, + "specVersion": 274, + "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", 5], + ["0xe65b00e46cedd0aa", 2], + ["0x42e62be4a39e5b60", 1], + ["0x806df4ccaa9ed485", 1], + ["0x8375104b299b74c5", 1], + ["0x5d1fbfbe852f2807", 1], + ["0xc6886e2f8e598b0a", 1], + ], + "transactionVersion": 1, + "stateVersion": 1, + }, + }, + }, + "chain_getBlockHash": { + "[5752019]": { + "jsonrpc": "2.0", + "result": "0x0ba73c8d5a0b78f7336a3e394a2a1fa7b7dec0924937b1dfb05a50ac3dd5cfc1", + } + }, + "chain_getBlock": { + '["0x0ba73c8d5a0b78f7336a3e394a2a1fa7b7dec0924937b1dfb05a50ac3dd5cfc1"]': { + "result": {"block": {}} + } + }, + }, +} diff --git a/tests/helpers/integration_websocket_metadata.txt b/tests/helpers/integration_websocket_metadata.txt new file mode 100644 index 0000000000..ef36fa747f --- /dev/null +++ b/tests/helpers/integration_websocket_metadata.txt @@ -0,0 +1 @@ +0x6d6574610e6107000c1c73705f636f72651863727970746f2c4163636f756e7449643332000004000401205b75383b2033325d0000040000032000000008000800000503000c08306672616d655f73797374656d2c4163636f756e74496e666f08144e6f6e636501102c4163636f756e74446174610114001401146e6f6e63651001144e6f6e6365000124636f6e73756d657273100120526566436f756e7400012470726f766964657273100120526566436f756e7400012c73756666696369656e7473100120526566436f756e740001106461746114012c4163636f756e74446174610000100000050500140c3c70616c6c65745f62616c616e6365731474797065732c4163636f756e7444617461041c42616c616e63650118001001106672656518011c42616c616e6365000120726573657276656418011c42616c616e636500011866726f7a656e18011c42616c616e6365000114666c6167731c01284578747261466c61677300001800000506001c0c3c70616c6c65745f62616c616e636573147479706573284578747261466c61677300000400200110753132380000200000050700240000050000280c346672616d655f737570706f7274206469737061746368405065724469737061746368436c617373040454012c000c01186e6f726d616c2c01045400012c6f7065726174696f6e616c2c0104540001246d616e6461746f72792c01045400002c0c2873705f77656967687473247765696768745f76321857656967687400000801207265665f74696d6530010c75363400012870726f6f665f73697a6530010c753634000030000006180034083c7072696d69746976655f74797065731048323536000004000401205b75383b2033325d00003800000208003c102873705f72756e74696d651c67656e65726963186469676573741844696765737400000401106c6f677340013c5665633c4469676573744974656d3e000040000002440044102873705f72756e74696d651c67656e6572696318646967657374284469676573744974656d0001142850726552756e74696d650800480144436f6e73656e737573456e67696e654964000038011c5665633c75383e00060024436f6e73656e7375730800480144436f6e73656e737573456e67696e654964000038011c5665633c75383e000400105365616c0800480144436f6e73656e737573456e67696e654964000038011c5665633c75383e000500144f74686572040038011c5665633c75383e0000006452756e74696d65456e7669726f6e6d656e745570646174656400080000480000030400000008004c00000250005008306672616d655f73797374656d2c4576656e745265636f7264080445015404540134000c011470686173655501011450686173650001146576656e7454010445000118746f70696373b401185665633c543e00005408586e6f64655f73756274656e736f725f72756e74696d653052756e74696d654576656e740001581853797374656d04005801706672616d655f73797374656d3a3a4576656e743c52756e74696d653e0000001c4772616e64706104007c015470616c6c65745f6772616e6470613a3a4576656e740004002042616c616e63657304008c017c70616c6c65745f62616c616e6365733a3a4576656e743c52756e74696d653e000500485472616e73616374696f6e5061796d656e7404009401a870616c6c65745f7472616e73616374696f6e5f7061796d656e743a3a4576656e743c52756e74696d653e0006003c53756274656e736f724d6f64756c65040098018070616c6c65745f73756274656e736f723a3a4576656e743c52756e74696d653e0007002c547269756d7669726174650400c001fc70616c6c65745f636f6c6c6563746976653a3a4576656e743c52756e74696d652c2070616c6c65745f636f6c6c6563746976653a3a496e7374616e6365313e00080048547269756d7669726174654d656d626572730400c401fc70616c6c65745f6d656d626572736869703a3a4576656e743c52756e74696d652c2070616c6c65745f6d656d626572736869703a3a496e7374616e6365313e0009003453656e6174654d656d626572730400c801fc70616c6c65745f6d656d626572736869703a3a4576656e743c52756e74696d652c2070616c6c65745f6d656d626572736869703a3a496e7374616e6365323e000a001c5574696c6974790400cc015470616c6c65745f7574696c6974793a3a4576656e74000b00105375646f0400d0016c70616c6c65745f7375646f3a3a4576656e743c52756e74696d653e000c00204d756c74697369670400d4017c70616c6c65745f6d756c74697369673a3a4576656e743c52756e74696d653e000d0020507265696d6167650400dc017c70616c6c65745f707265696d6167653a3a4576656e743c52756e74696d653e000e00245363686564756c65720400e0018070616c6c65745f7363686564756c65723a3a4576656e743c52756e74696d653e000f001450726f78790400ec017070616c6c65745f70726f78793a3a4576656e743c52756e74696d653e0010002052656769737472790400f4017c70616c6c65745f72656769737472793a3a4576656e743c52756e74696d653e0011002c436f6d6d69746d656e74730400f8018870616c6c65745f636f6d6d69746d656e74733a3a4576656e743c52756e74696d653e0012002841646d696e5574696c730400fc018870616c6c65745f61646d696e5f7574696c733a3a4576656e743c52756e74696d653e00130020536166654d6f646504000101018070616c6c65745f736166655f6d6f64653a3a4576656e743c52756e74696d653e00140020457468657265756d04000901015870616c6c65745f657468657265756d3a3a4576656e740015000c45564d04003501016870616c6c65745f65766d3a3a4576656e743c52756e74696d653e0016001c4261736546656504003d01015870616c6c65745f626173655f6665653a3a4576656e74001900144472616e6404004d01017070616c6c65745f6472616e643a3a4576656e743c52756e74696d653e001a0000580c306672616d655f73797374656d1870616c6c6574144576656e7404045400011c4045787472696e7369635375636365737304013464697370617463685f696e666f5c01304469737061746368496e666f00000490416e2065787472696e73696320636f6d706c65746564207375636365737366756c6c792e3c45787472696e7369634661696c656408013864697370617463685f6572726f7268013444697370617463684572726f7200013464697370617463685f696e666f5c01304469737061746368496e666f00010450416e2065787472696e736963206661696c65642e2c436f64655570646174656400020450603a636f6465602077617320757064617465642e284e65774163636f756e7404011c6163636f756e74000130543a3a4163636f756e7449640003046841206e6577206163636f756e742077617320637265617465642e344b696c6c65644163636f756e7404011c6163636f756e74000130543a3a4163636f756e74496400040458416e206163636f756e7420776173207265617065642e2052656d61726b656408011873656e646572000130543a3a4163636f756e7449640001106861736834011c543a3a48617368000504704f6e206f6e2d636861696e2072656d61726b2068617070656e65642e4455706772616465417574686f72697a6564080124636f64655f6861736834011c543a3a48617368000134636865636b5f76657273696f6e240110626f6f6c00060468416e20757067726164652077617320617574686f72697a65642e04704576656e7420666f72207468652053797374656d2070616c6c65742e5c0c346672616d655f737570706f7274206469737061746368304469737061746368496e666f00000c01187765696768742c0118576569676874000114636c6173736001344469737061746368436c617373000120706179735f666565640110506179730000600c346672616d655f737570706f7274206469737061746368344469737061746368436c61737300010c184e6f726d616c0000002c4f7065726174696f6e616c000100244d616e6461746f727900020000640c346672616d655f737570706f727420646973706174636810506179730001080c596573000000084e6f0001000068082873705f72756e74696d653444697370617463684572726f72000138144f746865720000003043616e6e6f744c6f6f6b7570000100244261644f726967696e000200184d6f64756c6504006c012c4d6f64756c654572726f7200030044436f6e73756d657252656d61696e696e670004002c4e6f50726f76696465727300050040546f6f4d616e79436f6e73756d65727300060014546f6b656e0400700128546f6b656e4572726f720007002841726974686d65746963040074013c41726974686d657469634572726f72000800345472616e73616374696f6e616c04007801485472616e73616374696f6e616c4572726f7200090024457868617573746564000a0028436f7272757074696f6e000b002c556e617661696c61626c65000c0038526f6f744e6f74416c6c6f776564000d00006c082873705f72756e74696d652c4d6f64756c654572726f720000080114696e64657808010875380001146572726f7248018c5b75383b204d41585f4d4f44554c455f4552524f525f454e434f4445445f53495a455d000070082873705f72756e74696d6528546f6b656e4572726f720001284046756e6473556e617661696c61626c65000000304f6e6c7950726f76696465720001003042656c6f774d696e696d756d0002003043616e6e6f7443726561746500030030556e6b6e6f776e41737365740004001846726f7a656e0005002c556e737570706f727465640006004043616e6e6f74437265617465486f6c64000700344e6f74457870656e6461626c650008001c426c6f636b65640009000074083473705f61726974686d657469633c41726974686d657469634572726f7200010c24556e646572666c6f77000000204f766572666c6f77000100384469766973696f6e42795a65726f0002000078082873705f72756e74696d65485472616e73616374696f6e616c4572726f72000108304c696d6974526561636865640000001c4e6f4c61796572000100007c0c3870616c6c65745f6772616e6470611870616c6c6574144576656e7400010c384e6577417574686f726974696573040134617574686f726974795f736574800134417574686f726974794c6973740000048c4e657720617574686f726974792073657420686173206265656e206170706c6965642e185061757365640001049843757272656e7420617574686f726974792073657420686173206265656e207061757365642e1c526573756d65640002049c43757272656e7420617574686f726974792073657420686173206265656e20726573756d65642e047c54686520604576656e746020656e756d206f6620746869732070616c6c65748000000284008400000408881800880c5073705f636f6e73656e7375735f6772616e6470610c617070185075626c69630000040004013c656432353531393a3a5075626c696300008c0c3c70616c6c65745f62616c616e6365731870616c6c6574144576656e740804540004490001581c456e646f77656408011c6163636f756e74000130543a3a4163636f756e744964000130667265655f62616c616e6365180128543a3a42616c616e6365000004b8416e206163636f756e74207761732063726561746564207769746820736f6d6520667265652062616c616e63652e20447573744c6f737408011c6163636f756e74000130543a3a4163636f756e744964000118616d6f756e74180128543a3a42616c616e63650001083d01416e206163636f756e74207761732072656d6f7665642077686f73652062616c616e636520776173206e6f6e2d7a65726f206275742062656c6f77204578697374656e7469616c4465706f7369742c78726573756c74696e6720696e20616e206f75747269676874206c6f73732e205472616e736665720c011066726f6d000130543a3a4163636f756e744964000108746f000130543a3a4163636f756e744964000118616d6f756e74180128543a3a42616c616e63650002044c5472616e73666572207375636365656465642e2842616c616e636553657408010c77686f000130543a3a4163636f756e74496400011066726565180128543a3a42616c616e636500030468412062616c616e6365207761732073657420627920726f6f742e20526573657276656408010c77686f000130543a3a4163636f756e744964000118616d6f756e74180128543a3a42616c616e6365000404e0536f6d652062616c616e63652077617320726573657276656420286d6f7665642066726f6d206672656520746f207265736572766564292e28556e726573657276656408010c77686f000130543a3a4163636f756e744964000118616d6f756e74180128543a3a42616c616e6365000504e8536f6d652062616c616e63652077617320756e726573657276656420286d6f7665642066726f6d20726573657276656420746f2066726565292e4852657365727665526570617472696174656410011066726f6d000130543a3a4163636f756e744964000108746f000130543a3a4163636f756e744964000118616d6f756e74180128543a3a42616c616e636500014864657374696e6174696f6e5f7374617475739001185374617475730006084d01536f6d652062616c616e636520776173206d6f7665642066726f6d207468652072657365727665206f6620746865206669727374206163636f756e7420746f20746865207365636f6e64206163636f756e742ed846696e616c20617267756d656e7420696e64696361746573207468652064657374696e6174696f6e2062616c616e636520747970652e1c4465706f73697408010c77686f000130543a3a4163636f756e744964000118616d6f756e74180128543a3a42616c616e6365000704d8536f6d6520616d6f756e7420776173206465706f73697465642028652e672e20666f72207472616e73616374696f6e2066656573292e20576974686472617708010c77686f000130543a3a4163636f756e744964000118616d6f756e74180128543a3a42616c616e63650008041d01536f6d6520616d6f756e74207761732077697468647261776e2066726f6d20746865206163636f756e742028652e672e20666f72207472616e73616374696f6e2066656573292e1c536c617368656408010c77686f000130543a3a4163636f756e744964000118616d6f756e74180128543a3a42616c616e63650009040101536f6d6520616d6f756e74207761732072656d6f7665642066726f6d20746865206163636f756e742028652e672e20666f72206d69736265686176696f72292e184d696e74656408010c77686f000130543a3a4163636f756e744964000118616d6f756e74180128543a3a42616c616e6365000a049c536f6d6520616d6f756e7420776173206d696e74656420696e746f20616e206163636f756e742e184275726e656408010c77686f000130543a3a4163636f756e744964000118616d6f756e74180128543a3a42616c616e6365000b049c536f6d6520616d6f756e7420776173206275726e65642066726f6d20616e206163636f756e742e2453757370656e64656408010c77686f000130543a3a4163636f756e744964000118616d6f756e74180128543a3a42616c616e6365000c041501536f6d6520616d6f756e74207761732073757370656e6465642066726f6d20616e206163636f756e74202869742063616e20626520726573746f726564206c61746572292e20526573746f72656408010c77686f000130543a3a4163636f756e744964000118616d6f756e74180128543a3a42616c616e6365000d04a4536f6d6520616d6f756e742077617320726573746f72656420696e746f20616e206163636f756e742e20557067726164656404010c77686f000130543a3a4163636f756e744964000e0460416e206163636f756e74207761732075706772616465642e18497373756564040118616d6f756e74180128543a3a42616c616e6365000f042d01546f74616c2069737375616e63652077617320696e637265617365642062792060616d6f756e74602c206372656174696e6720612063726564697420746f2062652062616c616e6365642e2452657363696e646564040118616d6f756e74180128543a3a42616c616e63650010042501546f74616c2069737375616e636520776173206465637265617365642062792060616d6f756e74602c206372656174696e672061206465627420746f2062652062616c616e6365642e184c6f636b656408010c77686f000130543a3a4163636f756e744964000118616d6f756e74180128543a3a42616c616e636500110460536f6d652062616c616e636520776173206c6f636b65642e20556e6c6f636b656408010c77686f000130543a3a4163636f756e744964000118616d6f756e74180128543a3a42616c616e636500120468536f6d652062616c616e63652077617320756e6c6f636b65642e1846726f7a656e08010c77686f000130543a3a4163636f756e744964000118616d6f756e74180128543a3a42616c616e636500130460536f6d652062616c616e6365207761732066726f7a656e2e1854686177656408010c77686f000130543a3a4163636f756e744964000118616d6f756e74180128543a3a42616c616e636500140460536f6d652062616c616e636520776173207468617765642e4c546f74616c49737375616e6365466f7263656408010c6f6c64180128543a3a42616c616e636500010c6e6577180128543a3a42616c616e6365001504ac5468652060546f74616c49737375616e6365602077617320666f72636566756c6c79206368616e6765642e047c54686520604576656e746020656e756d206f6620746869732070616c6c65749014346672616d655f737570706f72741874726169747318746f6b656e73106d6973633442616c616e6365537461747573000108104672656500000020526573657276656400010000940c6870616c6c65745f7472616e73616374696f6e5f7061796d656e741870616c6c6574144576656e74040454000104485472616e73616374696f6e466565506169640c010c77686f000130543a3a4163636f756e74496400012861637475616c5f66656518013042616c616e63654f663c543e00010c74697018013042616c616e63654f663c543e000008590141207472616e73616374696f6e20666565206061637475616c5f666565602c206f662077686963682060746970602077617320616464656420746f20746865206d696e696d756d20696e636c7573696f6e206665652c5c686173206265656e2070616964206279206077686f602e047c54686520604576656e746020656e756d206f6620746869732070616c6c6574980c4070616c6c65745f73756274656e736f721870616c6c6574144576656e7404045400015d01304e6574776f726b416464656408009c010c75313600009c010c7531360000045c61206e6577206e6574776f726b2069732061646465642e384e6574776f726b52656d6f76656404009c010c7531360001045461206e6574776f726b2069732072656d6f7665642e285374616b6541646465641400000130543a3a4163636f756e7449640000000130543a3a4163636f756e744964000018010c753634000018010c75363400009c010c75313600020459017374616b6520686173206265656e207472616e736665727265642066726f6d20746865206120636f6c646b6579206163636f756e74206f6e746f2074686520686f746b6579207374616b696e67206163636f756e742e305374616b6552656d6f7665641400000130543a3a4163636f756e7449640000000130543a3a4163636f756e744964000018010c753634000018010c75363400009c010c75313600030441017374616b6520686173206265656e2072656d6f7665642066726f6d2074686520686f746b6579207374616b696e67206163636f756e74206f6e746f2074686520636f6c646b6579206163636f756e742e285374616b654d6f7665641800000130543a3a4163636f756e7449640000000130543a3a4163636f756e74496400009c010c7531360000000130543a3a4163636f756e74496400009c010c753136000018010c753634000404c1017374616b6520686173206265656e206d6f7665642066726f6d206f726967696e2028686f746b65792c207375626e65742049442920746f2064657374696e6174696f6e2028686f746b65792c207375626e657420494429206f66207468697320616d6f756e742028696e2054414f292e285765696768747353657408009c010c75313600009c010c753136000504e4612063616c6c6572207375636365737366756c6c7920736574732074686569722077656967687473206f6e2061207375626e6574776f726b2e404e6575726f6e526567697374657265640c009c010c75313600009c010c7531360000000130543a3a4163636f756e744964000604d861206e6577206e6575726f6e206163636f756e7420686173206265656e207265676973746572656420746f2074686520636861696e2e5442756c6b4e6575726f6e735265676973746572656408009c010c75313600009c010c753136000704c06d756c7469706c6520756964732068617665206265656e20636f6e63757272656e746c7920726567697374657265642e3c42756c6b42616c616e63657353657408009c010c75313600009c010c7531360008044c4649584d453a204e6f74207573656420796574444d6178416c6c6f7765645569647353657408009c010c75313600009c010c753136000904bc6d617820616c6c6f776564207569647320686173206265656e2073657420666f722061207375626e6574776f726b2e444d61785765696768744c696d697453657408009c010c75313600009c010c753136000a04cc746865206d617820776569676874206c696d697420686173206265656e2073657420666f722061207375626e6574776f726b2e34446966666963756c747953657408009c010c753136000018010c753634000b04a474686520646966666963756c747920686173206265656e2073657420666f722061207375626e65742e5441646a7573746d656e74496e74657276616c53657408009c010c75313600009c010c753136000c04b07468652061646a7573746d656e7420696e74657276616c2069732073657420666f722061207375626e65742e68526567697374726174696f6e506572496e74657276616c53657408009c010c75313600009c010c753136000d04b8726567697374726174696f6e2070657220696e74657276616c2069732073657420666f722061207375626e65742e6c4d6178526567697374726174696f6e73506572426c6f636b53657408009c010c75313600009c010c753136000e048c776520736574206d617820726567697374726174696f6e732070657220626c6f636b2e4441637469766974794375746f666653657408009c010c75313600009c010c753136000f049c616e206163746976697479206375746f66662069732073657420666f722061207375626e65742e1852686f53657408009c010c75313600009c010c7531360010044452686f2076616c7565206973207365742e204b6170706153657408009c010c75313600009c010c753136001104684b617070612069732073657420666f722061207375626e65742e4c4d696e416c6c6f77656457656967687453657408009c010c75313600009c010c753136001204ac6d696e696d756d20616c6c6f776564207765696768742069732073657420666f722061207375626e65742e5056616c696461746f725072756e654c656e53657408009c010c753136000018010c753634001304a87468652076616c696461746f72207072756e696e67206c656e67746820686173206265656e207365742e485363616c696e674c6177506f77657253657408009c010c75313600009c010c753136001404c0746865207363616c696e67206c617720706f77657220686173206265656e2073657420666f722061207375626e65742e5857656967687473536574526174654c696d697453657408009c010c753136000018010c753634001504c477656967687473207365742072617465206c696d697420686173206265656e2073657420666f722061207375626e65742e44496d6d756e697479506572696f6453657408009c010c75313600009c010c75313600160490696d6d756e69747920706572696f642069732073657420666f722061207375626e65742e54426f6e64734d6f76696e674176657261676553657408009c010c753136000018010c753634001704a4626f6e6473206d6f76696e6720617665726167652069732073657420666f722061207375626e65742e3c426f6e647350656e616c747953657408009c010c75313600009c010c75313600180488626f6e64732070656e616c74792069732073657420666f722061207375626e65742e5c4d6178416c6c6f77656456616c696461746f727353657408009c010c75313600009c010c753136001904e473657474696e6720746865206d6178206e756d626572206f6620616c6c6f7765642076616c696461746f7273206f6e2061207375626e65742e2841786f6e53657276656408009c010c7531360000000130543a3a4163636f756e744964001a04d07468652061786f6e2073657276657220696e666f726d6174696f6e20697320616464656420746f20746865206e6574776f726b2e4050726f6d65746865757353657276656408009c010c7531360000000130543a3a4163636f756e744964001b04e87468652070726f6d6574686575732073657276657220696e666f726d6174696f6e20697320616464656420746f20746865206e6574776f726b2e44456d697373696f6e56616c756573536574001c04a0656d697373696f6e20726174696f7320666f7220616c6c206e6574776f726b73206973207365742e3444656c656761746541646465640c00000130543a3a4163636f756e7449640000000130543a3a4163636f756e74496400009c010c753136001d047c6120686f746b657920686173206265636f6d6520612064656c65676174652e3844656661756c7454616b6553657404009c010c753136001e04607468652064656661756c742074616b65206973207365742e505765696768747356657273696f6e4b657953657408009c010c753136000018010c753634001f04a4776569676874732076657273696f6e206b65792069732073657420666f722061206e6574776f726b2e404d696e446966666963756c747953657408009c010c753136000018010c7536340020049073657474696e67206d696e20646966666963756c7479206f6e2061206e6574776f726b2e404d6178446966666963756c747953657408009c010c753136000018010c7536340021049073657474696e67206d617820646966666963756c7479206f6e2061206e6574776f726b2e4c53657276696e67526174654c696d697453657408009c010c753136000018010c753634002204a873657474696e67207468652070726f6d6574686575732073657276696e672072617465206c696d69742e1c4275726e53657408009c010c753136000018010c7536340023046873657474696e67206275726e206f6e2061206e6574776f726b2e284d61784275726e53657408009c010c753136000018010c7536340024047873657474696e67206d6178206275726e206f6e2061206e6574776f726b2e284d696e4275726e53657408009c010c753136000018010c7536340025047873657474696e67206d696e206275726e206f6e2061206e6574776f726b2e385478526174654c696d6974536574040018010c7536340026048c73657474696e6720746865207472616e73616374696f6e2072617465206c696d69742e68547844656c656761746554616b65526174654c696d6974536574040018010c753634002704c473657474696e67207468652064656c65676174652074616b65207472616e73616374696f6e2072617465206c696d69742e6854784368696c644b657954616b65526174654c696d6974536574040018010c753634002804c473657474696e6720746865206368696c646b65792074616b65207472616e73616374696f6e2072617465206c696d69742e484d696e4368696c644b657954616b6553657404009c010c753136002904646d696e696d756d206368696c646b65792074616b6520736574484d61784368696c644b657954616b6553657404009c010c753136002a04646d6178696d756d206368696c646b65792074616b65207365743c4368696c644b657954616b655365740800000130543a3a4163636f756e74496400009c010c753136002b04446368696c646b65792074616b65207365741453756469640400a001384469737061746368526573756c74002c045061207375646f2063616c6c20697320646f6e652e4c526567697374726174696f6e416c6c6f77656408009c010c7531360000240110626f6f6c002d04c0726567697374726174696f6e20697320616c6c6f7765642f646973616c6c6f77656420666f722061207375626e65742e58506f77526567697374726174696f6e416c6c6f77656408009c010c7531360000240110626f6f6c002e04d0504f5720726567697374726174696f6e20697320616c6c6f7765642f646973616c6c6f77656420666f722061207375626e65742e2054656d706f53657408009c010c75313600009c010c753136002f046873657474696e672074656d706f206f6e2061206e6574776f726b7452414f52656379636c6564466f72526567697374726174696f6e53657408009c010c753136000018010c753634003004a873657474696e67207468652052414f2072656379636c656420666f7220726567697374726174696f6e2e445374616b655468726573686f6c64536574040018010c753634003104bc6d696e207374616b652069732073657420666f722076616c696461746f727320746f2073657420776569676874732e7453656e61746552657175697265645374616b6550657263656e74536574040018010c753634003204090173657474696e6720746865206d696e696d756d207265717569726564207374616b6520616d6f756e7420666f722073656e61746520726567697374726174696f6e2e4841646a7573746d656e74416c70686153657408009c010c753136000018010c753634003304a473657474696e67207468652061646a7573746d656e7420616c706861206f6e2061207375626e65742e184661756365740800000130543a3a4163636f756e744964000018010c75363400340494746865206661756365742069742063616c6c6564206f6e207468652074657374206e65742e445375626e65744f776e657243757453657404009c010c75313600350470746865207375626e6574206f776e657220637574206973207365742e4c4e6574776f726b526174654c696d6974536574040018010c7536340036049c746865206e6574776f726b206372656174696f6e2072617465206c696d6974206973207365742e604e6574776f726b496d6d756e697479506572696f64536574040018010c7536340037048c746865206e6574776f726b20696d6d756e69747920706572696f64206973207365742e544e6574776f726b4d696e4c6f636b436f7374536574040018010c753634003804a0746865206e6574776f726b206d696e696d756d206c6f636b696e6720636f7374206973207365742e385375626e65744c696d697453657404009c010c75313600390490746865206d6178696d756d206e756d626572206f66207375626e657473206973207365748c4e6574776f726b4c6f636b436f7374526564756374696f6e496e74657276616c536574040018010c753634003a0478746865206c6f636b20636f737420726564756374696f6e206973207365743454616b654465637265617365640c00000130543a3a4163636f756e7449640000000130543a3a4163636f756e74496400009c010c753136003b04947468652074616b6520666f7220612064656c6567617465206973206465637265617365642e3454616b65496e637265617365640c00000130543a3a4163636f756e7449640000000130543a3a4163636f756e74496400009c010c753136003c04947468652074616b6520666f7220612064656c656761746520697320696e637265617365642e34486f746b6579537761707065640c011c636f6c646b6579000130543a3a4163636f756e7449640464746865206163636f756e74204944206f6620636f6c646b657901286f6c645f686f746b6579000130543a3a4163636f756e7449640470746865206163636f756e74204944206f66206f6c6420686f746b657901286e65775f686f746b6579000130543a3a4163636f756e7449640470746865206163636f756e74204944206f66206e657720686f746b65793d045474686520686f746b65792069732073776170706564484d617844656c656761746554616b6553657404009c010c753136003e04d86d6178696d756d2064656c65676174652074616b6520697320736574206279207375646f2f61646d696e207472616e73616374696f6e484d696e44656c656761746554616b6553657404009c010c753136003f04d86d696e696d756d2064656c65676174652074616b6520697320736574206279207375646f2f61646d696e207472616e73616374696f6e3853656e61746541646a75737465640801286f6c645f6d656d626572a801504f7074696f6e3c543a3a4163636f756e7449643e04bc746865206163636f756e74204944206f6620746865206f6c642073656e617465206d656d6265722c20696620616e7901286e65775f6d656d626572000130543a3a4163636f756e744964049c746865206163636f756e74204944206f6620746865206e65772073656e617465206d656d62657240048861206d656d626572206f66207468652073656e6174652069732061646a757374656438436f6c646b6579537761707065640c012c6f6c645f636f6c646b6579000130543a3a4163636f756e7449640474746865206163636f756e74204944206f66206f6c6420636f6c646b6579012c6e65775f636f6c646b6579000130543a3a4163636f756e7449640474746865206163636f756e74204944206f66206e657720636f6c646b65790124737761705f636f737418010c7536340434746865207377617020636f73744104684120636f6c646b657920686173206265656e2073776170706564b0416c6c42616c616e6365556e7374616b6564416e645472616e73666572726564546f4e6577436f6c646b65790c013c63757272656e745f636f6c646b6579000130543a3a4163636f756e7449640494546865206163636f756e74204944206f66207468652063757272656e7420636f6c646b6579012c6e65775f636f6c646b6579000130543a3a4163636f756e7449640484546865206163636f756e74204944206f6620746865206e657720636f6c646b65790134746f74616c5f62616c616e6365180185013c3c5420617320436f6e6669673e3a3a43757272656e63792061732066756e6769626c653a3a496e73706563743c3c54206173206672616d655f73797374656d3a3a0a436f6e6669673e3a3a4163636f756e7449642c3e3e3a3a42616c616e6365047c54686520746f74616c2062616c616e6365206f662074686520686f746b657942042901416c6c2062616c616e6365206f66206120686f746b657920686173206265656e20756e7374616b656420616e64207472616e7366657272656420746f2061206e657720636f6c646b657950436f6c646b6579537761705363686564756c656410012c6f6c645f636f6c646b6579000130543a3a4163636f756e7449640484546865206163636f756e74204944206f6620746865206f6c6420636f6c646b6579012c6e65775f636f6c646b6579000130543a3a4163636f756e7449640484546865206163636f756e74204944206f6620746865206e657720636f6c646b6579013c657865637574696f6e5f626c6f636b100144426c6f636b4e756d626572466f723c543e04a8546865206172626974726174696f6e20626c6f636b20666f722074686520636f6c646b657920737761700124737761705f636f737418010c7536340434546865207377617020636f73744304844120636f6c646b6579207377617020686173206265656e207363686564756c6564644172626974726174696f6e506572696f64457874656e64656404011c636f6c646b6579000130543a3a4163636f756e7449640474546865206163636f756e74204944206f662074686520636f6c646b65794404a0546865206172626974726174696f6e20706572696f6420686173206265656e20657874656e646564505365744368696c6472656e5363686564756c65641000000130543a3a4163636f756e74496400009c010c753136000018010c7536340000ac01605665633c287536342c20543a3a4163636f756e744964293e004504cc53657474696e67206f66206368696c6472656e206f66206120686f746b65792068617665206265656e207363686564756c65642c5365744368696c6472656e0c00000130543a3a4163636f756e74496400009c010c7531360000ac01605665633c287536342c20543a3a4163636f756e744964293e00460498546865206368696c6472656e206f66206120686f746b65792068617665206265656e20736574484e6574776f726b4d61785374616b6553657408009c010c753136000018010c75363400470498546865206e6574776f726b206d6178696d756d207374616b6520686173206265656e2073657440436861696e4964656e746974795365740400000130543a3a4163636f756e74496400480498546865206964656e74697479206f66206120636f6c646b657920686173206265656e20736574445375626e65744964656e7469747953657404009c010c75313600490494546865206964656e74697479206f662061207375626e657420686173206265656e20736574545375626e65744964656e7469747952656d6f76656404009c010c753136004a04a4546865206964656e74697479206f662061207375626e657420686173206265656e2072656d6f76656460446973736f6c76654e6574776f726b5363686564756c65640c011c6163636f756e74000130543a3a4163636f756e74496404d8546865206163636f756e74204944207363686564756c652074686520646973736f6c7665206e6574776f726b206578747269736e696301186e65747569649c010c75313604706e6574776f726b2049442077696c6c20626520646973736f6c766564013c657865637574696f6e5f626c6f636b100144426c6f636b4e756d626572466f723c543e048065787472696e73696320657865637574696f6e20626c6f636b206e756d6265724b049c4120646973736f6c7665206e6574776f726b2065787472696e736963207363686564756c65642e78436f6c646b6579537761705363686564756c654475726174696f6e5365740400100144426c6f636b4e756d626572466f723c543e004c04c8546865206475726174696f6e206f66207363686564756c6520636f6c646b6579207377617020686173206265656e2073657488446973736f6c76654e6574776f726b5363686564756c654475726174696f6e5365740400100144426c6f636b4e756d626572466f723c543e004d04b4546865206475726174696f6e206f6620646973736f6c7665206e6574776f726b20686173206265656e20736574504352563357656967687473436f6d6d69747465640c00000130543a3a4163636f756e74496400009c010c753136000034011048323536004e14e8436f6d6d69742d72657665616c20763320776569676874732068617665206265656e207375636365737366756c6c7920636f6d6d69747465642e00f42d202a2a77686f2a2a3a20546865206163636f756e74204944206f6620746865207573657220636f6d6d697474696e672074686520776569676874732e942d202a2a6e65747569642a2a3a20546865206e6574776f726b206964656e7469666965722efc2d202a2a636f6d6d69745f686173682a2a3a20546865206861736820726570726573656e74696e672074686520636f6d6d697474656420776569676874732e4057656967687473436f6d6d69747465640c00000130543a3a4163636f756e74496400009c010c753136000034011048323536004f14a4576569676874732068617665206265656e207375636365737366756c6c7920636f6d6d69747465642e00f42d202a2a77686f2a2a3a20546865206163636f756e74204944206f6620746865207573657220636f6d6d697474696e672074686520776569676874732e942d202a2a6e65747569642a2a3a20546865206e6574776f726b206964656e7469666965722efc2d202a2a636f6d6d69745f686173682a2a3a20546865206861736820726570726573656e74696e672074686520636f6d6d697474656420776569676874732e3c5765696768747352657665616c65640c00000130543a3a4163636f756e74496400009c010c753136000034011048323536005014a0576569676874732068617665206265656e207375636365737366756c6c792072657665616c65642e00f02d202a2a77686f2a2a3a20546865206163636f756e74204944206f662074686520757365722072657665616c696e672074686520776569676874732e942d202a2a6e65747569642a2a3a20546865206e6574776f726b206964656e7469666965722ed02d202a2a636f6d6d69745f686173682a2a3a205468652068617368206f66207468652072657665616c656420776569676874732e5057656967687473426174636852657665616c65640c00000130543a3a4163636f756e74496400009c010c7531360000b401245665633c483235363e005114b8576569676874732068617665206265656e207375636365737366756c6c792062617463682072657665616c65642e00f02d202a2a77686f2a2a3a20546865206163636f756e74204944206f662074686520757365722072657665616c696e672074686520776569676874732e942d202a2a6e65747569642a2a3a20546865206e6574776f726b206964656e7469666965722e41012d202a2a72657665616c65645f6861736865732a2a3a204120766563746f72206f662068617368657320726570726573656e74696e6720656163682072657665616c656420776569676874207365742e54426174636857656967687473436f6d706c657465640800b801445665633c436f6d706163743c7531363e3e0000000130543a3a4163636f756e744964005210d041206261746368206f66207765696768747320286f7220636f6d6d697473292068617665206265656e20666f7263652d7365742e0035012d202a2a6e6574756964732a2a3a20546865206e65747569647320746865736520776569676874732077657265207375636365737366756c6c79207365742f636f6d6d697474656420666f722ea82d202a2a77686f2a2a3a2054686520686f746b657920746861742073657420746869732062617463682e604261746368436f6d706c65746564576974684572726f7273005304c4412062617463682065787472696e73696320636f6d706c6574656420627574207769746820736f6d65206572726f72732e5442617463685765696768744974656d4661696c6564040068016473705f72756e74696d653a3a44697370617463684572726f7200540cb441207765696768742073657420616d6f6e672061206261746368206f662077656967687473206661696c65642e00ec2d202a2a6572726f722a2a3a20546865206469737061746368206572726f7220656d697474656420627920746865206661696c6564206974656d2e405374616b655472616e736665727265641800000130543a3a4163636f756e7449640000000130543a3a4163636f756e7449640000000130543a3a4163636f756e74496400009c010c75313600009c010c753136000018010c75363400550c29015374616b6520686173206265656e207472616e736665727265642066726f6d206f6e6520636f6c646b657920746f20616e6f74686572206f6e207468652073616d65207375626e65742e2c506172616d65746572733a6101286f726967696e5f636f6c646b65792c2064657374696e6174696f6e5f636f6c646b65792c20686f746b65792c206f726967696e5f6e65747569642c2064657374696e6174696f6e5f6e65747569642c20616d6f756e7429305374616b65537761707065641400000130543a3a4163636f756e7449640000000130543a3a4163636f756e74496400009c010c75313600009c010c753136000018010c7536340056104d015374616b6520686173206265656e20737761707065642066726f6d206f6e65207375626e657420746f20616e6f7468657220666f72207468652073616d6520636f6c646b65792d686f746b657920706169722e002c506172616d65746572733af028636f6c646b65792c20686f746b65792c206f726967696e5f6e65747569642c2064657374696e6174696f6e5f6e65747569642c20616d6f756e7429047c54686520604576656e746020656e756d206f6620746869732070616c6c65749c0000050400a00418526573756c7408045401a4044501680108084f6b0400a4000000000c4572720400680000010000a40000040000a804184f7074696f6e04045401000108104e6f6e6500000010536f6d650400000000010000ac000002b000b000000408180000b40000023400b8000002bc00bc0000069c00c00c4470616c6c65745f636f6c6c6563746976651870616c6c6574144576656e7408045400044900011c2050726f706f73656410011c6163636f756e74000130543a3a4163636f756e7449640494546865206163636f756e7420746861742070726f706f73656420746865206d6f74696f6e2e013870726f706f73616c5f696e64657810013450726f706f73616c496e646578046854686520696e646578206f66207468652070726f706f73616c2e013470726f706f73616c5f6861736834011c543a3a4861736804645468652068617368206f66207468652070726f706f73616c2e01247468726573686f6c6410012c4d656d626572436f756e7404a4546865207468726573686f6c64206f66206d656d62657220666f72207468652070726f706f73616c2e0008490141206d6f74696f6e2028676976656e20686173682920686173206265656e2070726f706f7365642028627920676976656e206163636f756e742920776974682061207468726573686f6c642028676976656e3c604d656d626572436f756e7460292e14566f74656414011c6163636f756e74000130543a3a4163636f756e744964045c546865206163636f756e74207468617420766f7465642e013470726f706f73616c5f6861736834011c543a3a4861736804645468652068617368206f66207468652070726f706f73616c2e0114766f746564240110626f6f6c04785768657468657220746865206163636f756e7420766f746564206179652e010c79657310012c4d656d626572436f756e740460546865206e756d626572206f662079657320766f7465732e01086e6f10012c4d656d626572436f756e74045c546865206e756d626572206f66206e6f20766f7465732e0108050141206d6f74696f6e2028676976656e20686173682920686173206265656e20766f746564206f6e20627920676976656e206163636f756e742c206c656176696e671501612074616c6c79202879657320766f74657320616e64206e6f20766f74657320676976656e20726573706563746976656c7920617320604d656d626572436f756e7460292e20417070726f76656404013470726f706f73616c5f6861736834011c543a3a4861736804645468652068617368206f66207468652070726f706f73616c2e0204c041206d6f74696f6e2077617320617070726f76656420627920746865207265717569726564207468726573686f6c642e2c446973617070726f76656404013470726f706f73616c5f6861736834011c543a3a4861736804645468652068617368206f66207468652070726f706f73616c2e0304d041206d6f74696f6e20776173206e6f7420617070726f76656420627920746865207265717569726564207468726573686f6c642e20457865637574656408013470726f706f73616c5f6861736834011c543a3a4861736804645468652068617368206f66207468652070726f706f73616c2e0118726573756c74a001384469737061746368526573756c74047054686520726573756c74206f662074686520657865637574696f6e2e0404210141206d6f74696f6e207761732065786563757465643b20726573756c742077696c6c20626520604f6b602069662069742072657475726e656420776974686f7574206572726f722e384d656d626572457865637574656408013470726f706f73616c5f6861736834011c543a3a4861736804645468652068617368206f66207468652070726f706f73616c2e0118726573756c74a001384469737061746368526573756c74047054686520726573756c74206f662074686520657865637574696f6e2e05044901412073696e676c65206d656d6265722064696420736f6d6520616374696f6e3b20726573756c742077696c6c20626520604f6b602069662069742072657475726e656420776974686f7574206572726f722e18436c6f7365640c013470726f706f73616c5f6861736834011c543a3a4861736804645468652068617368206f66207468652070726f706f73616c2e010c79657310012c4d656d626572436f756e74048857686574686572207468652070726f706f73616c2077617320617070726f7665642e01086e6f10012c4d656d626572436f756e74048857686574686572207468652070726f706f73616c207761732072656a65637465642e06045501412070726f706f73616c2077617320636c6f736564206265636175736520697473207468726573686f6c64207761732072656163686564206f7220616674657220697473206475726174696f6e207761732075702e047c54686520604576656e746020656e756d206f6620746869732070616c6c6574c40c4470616c6c65745f6d656d626572736869701870616c6c6574144576656e740804540004490001182c4d656d6265724164646564000004e054686520676976656e206d656d626572207761732061646465643b2073656520746865207472616e73616374696f6e20666f722077686f2e344d656d62657252656d6f766564000104e854686520676976656e206d656d626572207761732072656d6f7665643b2073656520746865207472616e73616374696f6e20666f722077686f2e384d656d6265727353776170706564000204d854776f206d656d62657273207765726520737761707065643b2073656520746865207472616e73616374696f6e20666f722077686f2e304d656d6265727352657365740003041501546865206d656d62657273686970207761732072657365743b2073656520746865207472616e73616374696f6e20666f722077686f20746865206e6577207365742069732e284b65794368616e676564000404844f6e65206f6620746865206d656d6265727327206b657973206368616e6765642e1444756d6d790005046c5068616e746f6d206d656d6265722c206e6576657220757365642e047c54686520604576656e746020656e756d206f6620746869732070616c6c6574c80c4470616c6c65745f6d656d626572736869701870616c6c6574144576656e740804540004490001182c4d656d6265724164646564000004e054686520676976656e206d656d626572207761732061646465643b2073656520746865207472616e73616374696f6e20666f722077686f2e344d656d62657252656d6f766564000104e854686520676976656e206d656d626572207761732072656d6f7665643b2073656520746865207472616e73616374696f6e20666f722077686f2e384d656d6265727353776170706564000204d854776f206d656d62657273207765726520737761707065643b2073656520746865207472616e73616374696f6e20666f722077686f2e304d656d6265727352657365740003041501546865206d656d62657273686970207761732072657365743b2073656520746865207472616e73616374696f6e20666f722077686f20746865206e6577207365742069732e284b65794368616e676564000404844f6e65206f6620746865206d656d6265727327206b657973206368616e6765642e1444756d6d790005046c5068616e746f6d206d656d6265722c206e6576657220757365642e047c54686520604576656e746020656e756d206f6620746869732070616c6c6574cc0c3870616c6c65745f7574696c6974791870616c6c6574144576656e74000118404261746368496e746572727570746564080114696e64657810010c7533320001146572726f7268013444697370617463684572726f7200000855014261746368206f66206469737061746368657320646964206e6f7420636f6d706c6574652066756c6c792e20496e646578206f66206669727374206661696c696e6720646973706174636820676976656e2c2061734877656c6c20617320746865206572726f722e384261746368436f6d706c65746564000104c84261746368206f66206469737061746368657320636f6d706c657465642066756c6c792077697468206e6f206572726f722e604261746368436f6d706c65746564576974684572726f7273000204b44261746368206f66206469737061746368657320636f6d706c657465642062757420686173206572726f72732e344974656d436f6d706c657465640003041d01412073696e676c65206974656d2077697468696e2061204261746368206f6620646973706174636865732068617320636f6d706c657465642077697468206e6f206572726f722e284974656d4661696c65640401146572726f7268013444697370617463684572726f720004041101412073696e676c65206974656d2077697468696e2061204261746368206f6620646973706174636865732068617320636f6d706c657465642077697468206572726f722e30446973706174636865644173040118726573756c74a001384469737061746368526573756c7400050458412063616c6c2077617320646973706174636865642e047c54686520604576656e746020656e756d206f6620746869732070616c6c6574d00c2c70616c6c65745f7375646f1870616c6c6574144576656e7404045400011014537564696404012c7375646f5f726573756c74a001384469737061746368526573756c7404b454686520726573756c74206f66207468652063616c6c206d61646520627920746865207375646f20757365722e00047041207375646f2063616c6c206a75737420746f6f6b20706c6163652e284b65794368616e67656408010c6f6c64a801504f7074696f6e3c543a3a4163636f756e7449643e04b4546865206f6c64207375646f206b657920286966206f6e65207761732070726576696f75736c7920736574292e010c6e6577000130543a3a4163636f756e7449640488546865206e6577207375646f206b657920286966206f6e652077617320736574292e010478546865207375646f206b657920686173206265656e20757064617465642e284b657952656d6f76656400020480546865206b657920776173207065726d616e656e746c792072656d6f7665642e285375646f4173446f6e6504012c7375646f5f726573756c74a001384469737061746368526573756c7404b454686520726573756c74206f66207468652063616c6c206d61646520627920746865207375646f20757365722e0304c841205b7375646f5f61735d2850616c6c65743a3a7375646f5f6173292063616c6c206a75737420746f6f6b20706c6163652e047c54686520604576656e746020656e756d206f6620746869732070616c6c6574d40c3c70616c6c65745f6d756c74697369671870616c6c6574144576656e740404540001102c4e65774d756c74697369670c0124617070726f76696e67000130543a3a4163636f756e7449640001206d756c7469736967000130543a3a4163636f756e74496400012463616c6c5f6861736804012043616c6c486173680000048c41206e6577206d756c7469736967206f7065726174696f6e2068617320626567756e2e404d756c7469736967417070726f76616c100124617070726f76696e67000130543a3a4163636f756e74496400012474696d65706f696e74d8017054696d65706f696e743c426c6f636b4e756d626572466f723c543e3e0001206d756c7469736967000130543a3a4163636f756e74496400012463616c6c5f6861736804012043616c6c48617368000104c841206d756c7469736967206f7065726174696f6e20686173206265656e20617070726f76656420627920736f6d656f6e652e404d756c74697369674578656375746564140124617070726f76696e67000130543a3a4163636f756e74496400012474696d65706f696e74d8017054696d65706f696e743c426c6f636b4e756d626572466f723c543e3e0001206d756c7469736967000130543a3a4163636f756e74496400012463616c6c5f6861736804012043616c6c48617368000118726573756c74a001384469737061746368526573756c740002049c41206d756c7469736967206f7065726174696f6e20686173206265656e2065786563757465642e444d756c746973696743616e63656c6c656410012863616e63656c6c696e67000130543a3a4163636f756e74496400012474696d65706f696e74d8017054696d65706f696e743c426c6f636b4e756d626572466f723c543e3e0001206d756c7469736967000130543a3a4163636f756e74496400012463616c6c5f6861736804012043616c6c48617368000304a041206d756c7469736967206f7065726174696f6e20686173206265656e2063616e63656c6c65642e047c54686520604576656e746020656e756d206f6620746869732070616c6c6574d8083c70616c6c65745f6d756c74697369672454696d65706f696e74042c426c6f636b4e756d62657201100008011868656967687410012c426c6f636b4e756d626572000114696e64657810010c7533320000dc0c3c70616c6c65745f707265696d6167651870616c6c6574144576656e7404045400010c144e6f7465640401106861736834011c543a3a48617368000004684120707265696d61676520686173206265656e206e6f7465642e245265717565737465640401106861736834011c543a3a48617368000104784120707265696d61676520686173206265656e207265717565737465642e1c436c65617265640401106861736834011c543a3a486173680002046c4120707265696d616765206861732062656e20636c65617265642e047c54686520604576656e746020656e756d206f6620746869732070616c6c6574e00c4070616c6c65745f7363686564756c65721870616c6c6574144576656e74040454000124245363686564756c65640801107768656e100144426c6f636b4e756d626572466f723c543e000114696e64657810010c753332000004505363686564756c656420736f6d65207461736b2e2043616e63656c65640801107768656e100144426c6f636b4e756d626572466f723c543e000114696e64657810010c7533320001044c43616e63656c656420736f6d65207461736b2e28446973706174636865640c01107461736be401785461736b416464726573733c426c6f636b4e756d626572466f723c543e3e0001086964e801404f7074696f6e3c5461736b4e616d653e000118726573756c74a001384469737061746368526573756c74000204544469737061746368656420736f6d65207461736b2e2052657472795365741001107461736be401785461736b416464726573733c426c6f636b4e756d626572466f723c543e3e0001086964e801404f7074696f6e3c5461736b4e616d653e000118706572696f64100144426c6f636b4e756d626572466f723c543e00011c726574726965730801087538000304a0536574206120726574727920636f6e66696775726174696f6e20666f7220736f6d65207461736b2e38526574727943616e63656c6c65640801107461736be401785461736b416464726573733c426c6f636b4e756d626572466f723c543e3e0001086964e801404f7074696f6e3c5461736b4e616d653e000404ac43616e63656c206120726574727920636f6e66696775726174696f6e20666f7220736f6d65207461736b2e3c43616c6c556e617661696c61626c650801107461736be401785461736b416464726573733c426c6f636b4e756d626572466f723c543e3e0001086964e801404f7074696f6e3c5461736b4e616d653e00050429015468652063616c6c20666f72207468652070726f7669646564206861736820776173206e6f7420666f756e6420736f20746865207461736b20686173206265656e2061626f727465642e38506572696f6469634661696c65640801107461736be401785461736b416464726573733c426c6f636b4e756d626572466f723c543e3e0001086964e801404f7074696f6e3c5461736b4e616d653e0006043d0154686520676976656e207461736b2077617320756e61626c6520746f2062652072656e657765642073696e636520746865206167656e64612069732066756c6c206174207468617420626c6f636b2e2c52657472794661696c65640801107461736be401785461736b416464726573733c426c6f636b4e756d626572466f723c543e3e0001086964e801404f7074696f6e3c5461736b4e616d653e0007085d0154686520676976656e207461736b2077617320756e61626c6520746f20626520726574726965642073696e636520746865206167656e64612069732066756c6c206174207468617420626c6f636b206f722074686572659c776173206e6f7420656e6f7567682077656967687420746f2072657363686564756c652069742e545065726d616e656e746c794f7665727765696768740801107461736be401785461736b416464726573733c426c6f636b4e756d626572466f723c543e3e0001086964e801404f7074696f6e3c5461736b4e616d653e000804f054686520676976656e207461736b2063616e206e657665722062652065786563757465642073696e6365206974206973206f7665727765696768742e04304576656e747320747970652ee400000408101000e804184f7074696f6e04045401040108104e6f6e6500000010536f6d650400040000010000ec0c3070616c6c65745f70726f78791870616c6c6574144576656e740404540001143450726f78794578656375746564040118726573756c74a001384469737061746368526573756c74000004bc412070726f78792077617320657865637574656420636f72726563746c792c20776974682074686520676976656e2e2c507572654372656174656410011070757265000130543a3a4163636f756e74496400010c77686f000130543a3a4163636f756e74496400012870726f78795f74797065f00130543a3a50726f787954797065000150646973616d626967756174696f6e5f696e6465789c010c753136000108dc412070757265206163636f756e7420686173206265656e2063726561746564206279206e65772070726f7879207769746820676976656e90646973616d626967756174696f6e20696e64657820616e642070726f787920747970652e24416e6e6f756e6365640c01107265616c000130543a3a4163636f756e74496400011470726f7879000130543a3a4163636f756e74496400012463616c6c5f6861736834013443616c6c486173684f663c543e000204e0416e20616e6e6f756e63656d656e742077617320706c6163656420746f206d616b6520612063616c6c20696e20746865206675747572652e2850726f7879416464656410012464656c656761746f72000130543a3a4163636f756e74496400012464656c656761746565000130543a3a4163636f756e74496400012870726f78795f74797065f00130543a3a50726f78795479706500011464656c6179100144426c6f636b4e756d626572466f723c543e00030448412070726f7879207761732061646465642e3050726f787952656d6f76656410012464656c656761746f72000130543a3a4163636f756e74496400012464656c656761746565000130543a3a4163636f756e74496400012870726f78795f74797065f00130543a3a50726f78795479706500011464656c6179100144426c6f636b4e756d626572466f723c543e00040450412070726f7879207761732072656d6f7665642e047c54686520604576656e746020656e756d206f6620746869732070616c6c6574f008586e6f64655f73756274656e736f725f72756e74696d652450726f78795479706500013c0c416e79000000144f776e65720001002c4e6f6e437269746963616c0002002c4e6f6e5472616e736665720003001853656e617465000400304e6f6e46756e676962696c650005002c547269756d76697261746500060028476f7665726e616e63650007001c5374616b696e6700080030526567697374726174696f6e000900205472616e73666572000a0034536d616c6c5472616e73666572000b002c526f6f7457656967687473000c00244368696c644b657973000d00505375646f556e636865636b6564536574436f6465000e0000f40c3c70616c6c65745f72656769737472791870616c6c6574144576656e740404540001082c4964656e7469747953657404010c77686f000130543a3a4163636f756e74496404a0546865206163636f756e742074686174207265676973746572656420746865206964656e746974790004a4456d6974746564207768656e206120757365722072656769737465727320616e206964656e74697479444964656e74697479446973736f6c76656404010c77686f000130543a3a4163636f756e744964049c546865206163636f756e74207468617420646973736f6c76656420746865206964656e746974790104a4456d6974746564207768656e2061207573657220646973736f6c76657320616e206964656e74697479047c54686520604576656e746020656e756d206f6620746869732070616c6c6574f80c4870616c6c65745f636f6d6d69746d656e74731870616c6c6574144576656e7404045400010428436f6d6d69746d656e740801186e65747569649c010c7531360470546865206e6574756964206f662074686520636f6d6d69746d656e74010c77686f000130543a3a4163636f756e744964042c546865206163636f756e740004504120636f6d6d69746d656e742077617320736574047c54686520604576656e746020656e756d206f6620746869732070616c6c6574fc0c4870616c6c65745f61646d696e5f7574696c731870616c6c6574144576656e74040454000100047c54686520604576656e746020656e756d206f6620746869732070616c6c657401010c4070616c6c65745f736166655f6d6f64651870616c6c6574144576656e740404540001201c456e7465726564040114756e74696c100144426c6f636b4e756d626572466f723c543e000004dc54686520736166652d6d6f64652077617320656e746572656420756e74696c20696e636c75736976656c79207468697320626c6f636b2e20457874656e646564040114756e74696c100144426c6f636b4e756d626572466f723c543e000104e054686520736166652d6d6f64652077617320657874656e64656420756e74696c20696e636c75736976656c79207468697320626c6f636b2e18457869746564040118726561736f6e0501012845786974526561736f6e000204ac4578697465642074686520736166652d6d6f646520666f72206120737065636966696320726561736f6e2e344465706f736974506c6163656408011c6163636f756e74000130543a3a4163636f756e744964000118616d6f756e7418013042616c616e63654f663c543e0003042501416e206163636f756e742072657365727665642066756e647320666f722065697468657220656e746572696e67206f7220657874656e64696e672074686520736166652d6d6f64652e3c4465706f73697452656c656173656408011c6163636f756e74000130543a3a4163636f756e744964000118616d6f756e7418013042616c616e63654f663c543e000404d0416e206163636f756e7420686164206120726573657276652072656c65617365642074686174207761732072657365727665642e384465706f736974536c617368656408011c6163636f756e74000130543a3a4163636f756e744964000118616d6f756e7418013042616c616e63654f663c543e000504c4416e206163636f756e7420686164207265736572766520736c61736865642074686174207761732072657365727665642e3443616e6e6f744465706f73697400060cf4436f756c64206e6f7420686f6c642066756e647320666f7220656e746572696e67206f7220657874656e64696e672074686520736166652d6d6f64652e00c054686973206572726f7220636f6d65732066726f6d2074686520756e6465726c79696e67206043757272656e6379602e3443616e6e6f7452656c6561736500070c0101436f756c64206e6f742072656c656173652066756e647320666f7220656e746572696e67206f7220657874656e64696e672074686520736166652d6d6f64652e00c054686973206572726f7220636f6d65732066726f6d2074686520756e6465726c79696e67206043757272656e6379602e047c54686520604576656e746020656e756d206f6620746869732070616c6c657405010c4070616c6c65745f736166655f6d6f64651870616c6c65742845786974526561736f6e0001081c54696d656f757400000014466f7263650001000009010c3c70616c6c65745f657468657265756d1870616c6c6574144576656e7400010420457865637574656414011066726f6d0d01011048313630000108746f0d010110483136300001407472616e73616374696f6e5f686173683401104832353600012c657869745f726561736f6e1501012845786974526561736f6e00012865787472615f6461746138011c5665633c75383e000004c8416e20657468657265756d207472616e73616374696f6e20776173207375636365737366756c6c792065786563757465642e047c54686520604576656e746020656e756d206f6620746869732070616c6c65740d01083c7072696d69746976655f7479706573104831363000000400110101205b75383b2032305d0000110100000314000000080015010c2065766d5f636f7265146572726f722845786974526561736f6e0001101c5375636365656404001901012c4578697453756363656564000000144572726f7204001d010124457869744572726f720001001852657665727404002d0101284578697452657665727400020014466174616c04003101012445786974466174616c0003000019010c2065766d5f636f7265146572726f722c457869745375636365656400010c1c53746f707065640000002052657475726e6564000100205375696369646564000200001d010c2065766d5f636f7265146572726f7224457869744572726f7200014038537461636b556e646572666c6f7700000034537461636b4f766572666c6f770001002c496e76616c69644a756d7000020030496e76616c696452616e67650003004444657369676e61746564496e76616c69640004002c43616c6c546f6f446565700005003c437265617465436f6c6c6973696f6e0006004c437265617465436f6e74726163744c696d69740007002c496e76616c6964436f64650400210101184f70636f6465000f002c4f75744f664f6666736574000800204f75744f66476173000900244f75744f6646756e64000a002c5043556e646572666c6f77000b002c437265617465456d707479000c00144f74686572040025010144436f773c277374617469632c207374723e000d00204d61784e6f6e6365000e000021010c2065766d5f636f7265186f70636f6465184f70636f646500000400080108753800002501040c436f770404540129010004002901000000290100000502002d010c2065766d5f636f7265146572726f7228457869745265766572740001042052657665727465640000000031010c2065766d5f636f7265146572726f722445786974466174616c000110304e6f74537570706f7274656400000048556e68616e646c6564496e746572727570740001004043616c6c4572726f724173466174616c04001d010124457869744572726f72000200144f74686572040025010144436f773c277374617469632c207374723e0003000035010c2870616c6c65745f65766d1870616c6c6574144576656e740404540001140c4c6f6704010c6c6f673901010c4c6f670000047c457468657265756d206576656e74732066726f6d20636f6e7472616374732e1c4372656174656404011c616464726573730d01011048313630000104b44120636f6e747261637420686173206265656e206372656174656420617420676976656e20616464726573732e34437265617465644661696c656404011c616464726573730d0101104831363000020405014120636f6e74726163742077617320617474656d7074656420746f20626520637265617465642c206275742074686520657865637574696f6e206661696c65642e20457865637574656404011c616464726573730d01011048313630000304f84120636f6e747261637420686173206265656e206578656375746564207375636365737366756c6c79207769746820737461746573206170706c6965642e3845786563757465644661696c656404011c616464726573730d0101104831363000040465014120636f6e747261637420686173206265656e2065786563757465642077697468206572726f72732e20537461746573206172652072657665727465642077697468206f6e6c79206761732066656573206170706c6965642e047c54686520604576656e746020656e756d206f6620746869732070616c6c657439010c20657468657265756d0c6c6f670c4c6f6700000c011c616464726573730d01011048313630000118746f70696373b401245665633c483235363e00011064617461380114427974657300003d010c3c70616c6c65745f626173655f6665651870616c6c6574144576656e7400010c404e65774261736546656550657247617304010c66656541010110553235360000003c426173654665654f766572666c6f77000100344e6577456c6173746963697479040128656c61737469636974794901011c5065726d696c6c000200047c54686520604576656e746020656e756d206f6620746869732070616c6c65744101083c7072696d69746976655f7479706573105532353600000400450101205b7536343b20345d0000450100000304000000180049010c3473705f61726974686d65746963287065725f7468696e67731c5065726d696c6c0000040010010c75333200004d010c3070616c6c65745f6472616e641870616c6c6574144576656e740404540001084c426561636f6e436f6e6669674368616e676564000000204e657750756c7365040118726f756e6473510101405665633c526f756e644e756d6265723e000104805375636365737366756c6c79207365742061206e65772070756c73652873292e047c54686520604576656e746020656e756d206f6620746869732070616c6c657451010000021800550108306672616d655f73797374656d14506861736500010c384170706c7945787472696e736963040010010c7533320000003046696e616c697a6174696f6e00010038496e697469616c697a6174696f6e000200005901000002e4005d0108306672616d655f73797374656d584c61737452756e74696d6555706772616465496e666f0000080130737065635f76657273696f6e6101014c636f6465633a3a436f6d706163743c7533323e000124737065635f6e616d652901016473705f72756e74696d653a3a52756e74696d65537472696e67000061010000061000650108306672616d655f73797374656d60436f646555706772616465417574686f72697a6174696f6e0404540000080124636f64655f6861736834011c543a3a48617368000134636865636b5f76657273696f6e240110626f6f6c000069010c306672616d655f73797374656d1870616c6c65741043616c6c04045400012c1872656d61726b04011872656d61726b38011c5665633c75383e00000c684d616b6520736f6d65206f6e2d636861696e2072656d61726b2e008843616e20626520657865637574656420627920657665727920606f726967696e602e387365745f686561705f7061676573040114706167657318010c753634000104f853657420746865206e756d626572206f6620706167657320696e2074686520576562417373656d626c7920656e7669726f6e6d656e74277320686561702e207365745f636f6465040110636f646538011c5665633c75383e0002046453657420746865206e65772072756e74696d6520636f64652e5c7365745f636f64655f776974686f75745f636865636b73040110636f646538011c5665633c75383e000310190153657420746865206e65772072756e74696d6520636f646520776974686f757420646f696e6720616e7920636865636b73206f662074686520676976656e2060636f6465602e0051014e6f746520746861742072756e74696d652075706772616465732077696c6c206e6f742072756e20696620746869732069732063616c6c656420776974682061206e6f742d696e6372656173696e6720737065632076657273696f6e212c7365745f73746f726167650401146974656d736d0101345665633c4b657956616c75653e0004046853657420736f6d65206974656d73206f662073746f726167652e306b696c6c5f73746f726167650401106b657973750101205665633c4b65793e000504744b696c6c20736f6d65206974656d732066726f6d2073746f726167652e2c6b696c6c5f70726566697808011870726566697838010c4b657900011c7375626b65797310010c75333200061011014b696c6c20616c6c2073746f72616765206974656d7320776974682061206b657920746861742073746172747320776974682074686520676976656e207072656669782e0039012a2a4e4f54453a2a2a2057652072656c79206f6e2074686520526f6f74206f726967696e20746f2070726f7669646520757320746865206e756d626572206f66207375626b65797320756e6465723d0174686520707265666978207765206172652072656d6f76696e6720746f2061636375726174656c792063616c63756c6174652074686520776569676874206f6620746869732066756e6374696f6e2e4472656d61726b5f776974685f6576656e7404011872656d61726b38011c5665633c75383e000704a44d616b6520736f6d65206f6e2d636861696e2072656d61726b20616e6420656d6974206576656e742e44617574686f72697a655f75706772616465040124636f64655f6861736834011c543a3a486173680009106101417574686f72697a6520616e207570677261646520746f206120676976656e2060636f64655f686173686020666f72207468652072756e74696d652e205468652072756e74696d652063616e20626520737570706c696564186c617465722e007c546869732063616c6c20726571756972657320526f6f74206f726967696e2e80617574686f72697a655f757067726164655f776974686f75745f636865636b73040124636f64655f6861736834011c543a3a48617368000a206101417574686f72697a6520616e207570677261646520746f206120676976656e2060636f64655f686173686020666f72207468652072756e74696d652e205468652072756e74696d652063616e20626520737570706c696564186c617465722e005d015741524e494e473a205468697320617574686f72697a657320616e207570677261646520746861742077696c6c2074616b6520706c61636520776974686f757420616e792073616665747920636865636b732c20666f7259016578616d706c652074686174207468652073706563206e616d652072656d61696e73207468652073616d6520616e642074686174207468652076657273696f6e206e756d62657220696e637265617365732e204e6f74f07265636f6d6d656e64656420666f72206e6f726d616c207573652e205573652060617574686f72697a655f757067726164656020696e73746561642e007c546869732063616c6c20726571756972657320526f6f74206f726967696e2e606170706c795f617574686f72697a65645f75706772616465040110636f646538011c5665633c75383e000b24550150726f766964652074686520707265696d616765202872756e74696d652062696e617279292060636f64656020666f7220616e2075706772616465207468617420686173206265656e20617574686f72697a65642e00490149662074686520617574686f72697a6174696f6e20726571756972656420612076657273696f6e20636865636b2c20746869732063616c6c2077696c6c20656e73757265207468652073706563206e616d65e872656d61696e7320756e6368616e67656420616e6420746861742074686520737065632076657273696f6e2068617320696e637265617365642e005901446570656e64696e67206f6e207468652072756e74696d65277320604f6e536574436f64656020636f6e66696775726174696f6e2c20746869732066756e6374696f6e206d6179206469726563746c79206170706c791101746865206e65772060636f64656020696e207468652073616d6520626c6f636b206f7220617474656d707420746f207363686564756c652074686520757067726164652e0060416c6c206f726967696e732061726520616c6c6f7765642e040d01436f6e7461696e7320612076617269616e742070657220646973706174636861626c652065787472696e736963207468617420746869732070616c6c6574206861732e6d010000027101007101000004083838007501000002380079010c306672616d655f73797374656d186c696d69747330426c6f636b5765696768747300000c0128626173655f626c6f636b2c01185765696768740001246d61785f626c6f636b2c01185765696768740001247065725f636c6173737d0101845065724469737061746368436c6173733c57656967687473506572436c6173733e00007d010c346672616d655f737570706f7274206469737061746368405065724469737061746368436c617373040454018101000c01186e6f726d616c810101045400012c6f7065726174696f6e616c81010104540001246d616e6461746f72798101010454000081010c306672616d655f73797374656d186c696d6974733c57656967687473506572436c6173730000100138626173655f65787472696e7369632c01185765696768740001346d61785f65787472696e736963850101384f7074696f6e3c5765696768743e0001246d61785f746f74616c850101384f7074696f6e3c5765696768743e0001207265736572766564850101384f7074696f6e3c5765696768743e0000850104184f7074696f6e040454012c0108104e6f6e6500000010536f6d6504002c000001000089010c306672616d655f73797374656d186c696d6974732c426c6f636b4c656e677468000004010c6d61788d0101545065724469737061746368436c6173733c7533323e00008d010c346672616d655f737570706f7274206469737061746368405065724469737061746368436c6173730404540110000c01186e6f726d616c1001045400012c6f7065726174696f6e616c100104540001246d616e6461746f72791001045400009101082873705f776569676874733c52756e74696d65446257656967687400000801107265616418010c753634000114777269746518010c75363400009501082873705f76657273696f6e3852756e74696d6556657273696f6e0000200124737065635f6e616d652901013452756e74696d65537472696e67000124696d706c5f6e616d652901013452756e74696d65537472696e67000144617574686f72696e675f76657273696f6e10010c753332000130737065635f76657273696f6e10010c753332000130696d706c5f76657273696f6e10010c753332000110617069739901011c4170697356656300014c7472616e73616374696f6e5f76657273696f6e10010c75333200013473746174655f76657273696f6e080108753800009901040c436f77040454019d010004009d010000009d01000002a10100a10100000408a5011000a501000003080000000800a9010c306672616d655f73797374656d1870616c6c6574144572726f720404540001243c496e76616c6964537065634e616d650000081101546865206e616d65206f662073706563696669636174696f6e20646f6573206e6f74206d61746368206265747765656e207468652063757272656e742072756e74696d6550616e6420746865206e65772072756e74696d652e685370656356657273696f6e4e65656473546f496e63726561736500010841015468652073706563696669636174696f6e2076657273696f6e206973206e6f7420616c6c6f77656420746f206465637265617365206265747765656e207468652063757272656e742072756e74696d6550616e6420746865206e65772072756e74696d652e744661696c6564546f4578747261637452756e74696d6556657273696f6e00020cec4661696c656420746f2065787472616374207468652072756e74696d652076657273696f6e2066726f6d20746865206e65772072756e74696d652e0009014569746865722063616c6c696e672060436f72655f76657273696f6e60206f72206465636f64696e67206052756e74696d6556657273696f6e60206661696c65642e4c4e6f6e44656661756c74436f6d706f73697465000304fc537569636964652063616c6c6564207768656e20746865206163636f756e7420686173206e6f6e2d64656661756c7420636f6d706f7369746520646174612e3c4e6f6e5a65726f526566436f756e74000404350154686572652069732061206e6f6e2d7a65726f207265666572656e636520636f756e742070726576656e74696e6720746865206163636f756e742066726f6d206265696e67207075726765642e3043616c6c46696c7465726564000504d0546865206f726967696e2066696c7465722070726576656e74207468652063616c6c20746f20626520646973706174636865642e6c4d756c7469426c6f636b4d6967726174696f6e734f6e676f696e67000604550141206d756c74692d626c6f636b206d6967726174696f6e206973206f6e676f696e6720616e642070726576656e7473207468652063757272656e7420636f64652066726f6d206265696e67207265706c616365642e444e6f7468696e67417574686f72697a6564000704584e6f207570677261646520617574686f72697a65642e30556e617574686f72697a656400080494546865207375626d697474656420636f6465206973206e6f7420617574686f72697a65642e046c4572726f7220666f72207468652053797374656d2070616c6c6574ad010c4c626f756e6465645f636f6c6c656374696f6e732c626f756e6465645f76656328426f756e6465645665630804540134045300000400b401185665633c543e0000b1010c4070616c6c65745f74696d657374616d701870616c6c65741043616c6c0404540001040c73657404010c6e6f77300124543a3a4d6f6d656e7400004c54536574207468652063757272656e742074696d652e005501546869732063616c6c2073686f756c6420626520696e766f6b65642065786163746c79206f6e63652070657220626c6f636b2e2049742077696c6c2070616e6963206174207468652066696e616c697a6174696f6ed470686173652c20696620746869732063616c6c206861736e2774206265656e20696e766f6b656420627920746861742074696d652e0041015468652074696d657374616d702073686f756c642062652067726561746572207468616e207468652070726576696f7573206f6e652062792074686520616d6f756e7420737065636966696564206279685b60436f6e6669673a3a4d696e696d756d506572696f64605d2e00c4546865206469737061746368206f726967696e20666f7220746869732063616c6c206d757374206265205f4e6f6e655f2e0051015468697320646973706174636820636c617373206973205f4d616e6461746f72795f20746f20656e73757265206974206765747320657865637574656420696e2074686520626c6f636b2e204265206177617265510174686174206368616e67696e672074686520636f6d706c6578697479206f6620746869732063616c6c20636f756c6420726573756c742065786861757374696e6720746865207265736f757263657320696e206184626c6f636b20746f206578656375746520616e79206f746865722063616c6c732e0034232320436f6d706c657869747931012d20604f2831296020284e6f7465207468617420696d706c656d656e746174696f6e73206f6620604f6e54696d657374616d7053657460206d75737420616c736f20626520604f283129602955012d20312073746f72616765207265616420616e6420312073746f72616765206d75746174696f6e2028636f64656320604f283129602062656361757365206f6620604469645570646174653a3a74616b656020696e402020606f6e5f66696e616c697a656029d42d2031206576656e742068616e646c657220606f6e5f74696d657374616d705f736574602e204d75737420626520604f283129602e040d01436f6e7461696e7320612076617269616e742070657220646973706174636861626c652065787472696e736963207468617420746869732070616c6c6574206861732eb5010c4c626f756e6465645f636f6c6c656374696f6e732c626f756e6465645f76656328426f756e64656456656308045401b901045300000400bd0101185665633c543e0000b901104473705f636f6e73656e7375735f617572611c737232353531392c6170705f73723235353139185075626c69630000040004013c737232353531393a3a5075626c69630000bd01000002b90100c101084873705f636f6e73656e7375735f736c6f747310536c6f740000040018010c7536340000c501083870616c6c65745f6772616e6470612c53746f726564537461746504044e01100110104c6976650000003050656e64696e6750617573650801307363686564756c65645f61741001044e00011464656c61791001044e000100185061757365640002003450656e64696e67526573756d650801307363686564756c65645f61741001044e00011464656c61791001044e00030000c901083870616c6c65745f6772616e6470614c53746f72656450656e64696e674368616e676508044e0110144c696d697400001001307363686564756c65645f61741001044e00011464656c61791001044e0001406e6578745f617574686f726974696573cd01016c426f756e646564417574686f726974794c6973743c4c696d69743e000118666f72636564d10101244f7074696f6e3c4e3e0000cd010c4c626f756e6465645f636f6c6c656374696f6e73407765616b5f626f756e6465645f766563385765616b426f756e64656456656308045401840453000004008001185665633c543e0000d10104184f7074696f6e04045401100108104e6f6e6500000010536f6d650400100000010000d5010c3870616c6c65745f6772616e6470611870616c6c65741043616c6c04045400010c4c7265706f72745f65717569766f636174696f6e08014865717569766f636174696f6e5f70726f6f66d90101c8426f783c45717569766f636174696f6e50726f6f663c543a3a486173682c20426c6f636b4e756d626572466f723c543e3e3e00013c6b65795f6f776e65725f70726f6f6601020140543a3a4b65794f776e657250726f6f6600001009015265706f727420766f7465722065717569766f636174696f6e2f6d69736265686176696f722e2054686973206d6574686f642077696c6c2076657269667920746865f465717569766f636174696f6e2070726f6f6620616e642076616c69646174652074686520676976656e206b6579206f776e6572736869702070726f6f66f8616761696e73742074686520657874726163746564206f6666656e6465722e20496620626f7468206172652076616c69642c20746865206f6666656e63654477696c6c206265207265706f727465642e707265706f72745f65717569766f636174696f6e5f756e7369676e656408014865717569766f636174696f6e5f70726f6f66d90101c8426f783c45717569766f636174696f6e50726f6f663c543a3a486173682c20426c6f636b4e756d626572466f723c543e3e3e00013c6b65795f6f776e65725f70726f6f6601020140543a3a4b65794f776e657250726f6f6600012409015265706f727420766f7465722065717569766f636174696f6e2f6d69736265686176696f722e2054686973206d6574686f642077696c6c2076657269667920746865f465717569766f636174696f6e2070726f6f6620616e642076616c69646174652074686520676976656e206b6579206f776e6572736869702070726f6f66f8616761696e73742074686520657874726163746564206f6666656e6465722e20496620626f7468206172652076616c69642c20746865206f6666656e63654477696c6c206265207265706f727465642e000d01546869732065787472696e736963206d7573742062652063616c6c656420756e7369676e656420616e642069742069732065787065637465642074686174206f6e6c791501626c6f636b20617574686f72732077696c6c2063616c6c206974202876616c69646174656420696e206056616c6964617465556e7369676e656460292c2061732073756368150169662074686520626c6f636b20617574686f7220697320646566696e65642069742077696c6c20626520646566696e6564206173207468652065717569766f636174696f6e247265706f727465722e306e6f74655f7374616c6c656408011464656c6179100144426c6f636b4e756d626572466f723c543e00016c626573745f66696e616c697a65645f626c6f636b5f6e756d626572100144426c6f636b4e756d626572466f723c543e0002303d014e6f74652074686174207468652063757272656e7420617574686f7269747920736574206f6620746865204752414e4450412066696e616c6974792067616467657420686173207374616c6c65642e006101546869732077696c6c2074726967676572206120666f7263656420617574686f7269747920736574206368616e67652061742074686520626567696e6e696e67206f6620746865206e6578742073657373696f6e2c20746f6101626520656e6163746564206064656c61796020626c6f636b7320616674657220746861742e20546865206064656c6179602073686f756c64206265206869676820656e6f75676820746f20736166656c7920617373756d654901746861742074686520626c6f636b207369676e616c6c696e672074686520666f72636564206368616e67652077696c6c206e6f742062652072652d6f7267656420652e672e203130303020626c6f636b732e5d0154686520626c6f636b2070726f64756374696f6e207261746520287768696368206d617920626520736c6f77656420646f776e2062656361757365206f662066696e616c697479206c616767696e67292073686f756c64510162652074616b656e20696e746f206163636f756e74207768656e2063686f6f73696e6720746865206064656c6179602e20546865204752414e44504120766f74657273206261736564206f6e20746865206e65775501617574686f726974792077696c6c20737461727420766f74696e67206f6e20746f70206f662060626573745f66696e616c697a65645f626c6f636b5f6e756d6265726020666f72206e65772066696e616c697a65644d01626c6f636b732e2060626573745f66696e616c697a65645f626c6f636b5f6e756d626572602073686f756c64206265207468652068696768657374206f6620746865206c61746573742066696e616c697a6564c4626c6f636b206f6620616c6c2076616c696461746f7273206f6620746865206e657720617574686f72697479207365742e00584f6e6c792063616c6c61626c6520627920726f6f742e040d01436f6e7461696e7320612076617269616e742070657220646973706174636861626c652065787472696e736963207468617420746869732070616c6c6574206861732ed901085073705f636f6e73656e7375735f6772616e6470614445717569766f636174696f6e50726f6f660804480134044e0110000801187365745f6964180114536574496400013065717569766f636174696f6edd01014845717569766f636174696f6e3c482c204e3e0000dd01085073705f636f6e73656e7375735f6772616e6470613045717569766f636174696f6e0804480134044e011001081c507265766f74650400e10101890166696e616c6974795f6772616e6470613a3a45717569766f636174696f6e3c417574686f7269747949642c2066696e616c6974795f6772616e6470613a3a507265766f74653c0a482c204e3e2c20417574686f726974795369676e61747572652c3e00000024507265636f6d6d69740400f50101910166696e616c6974795f6772616e6470613a3a45717569766f636174696f6e3c417574686f7269747949642c2066696e616c6974795f6772616e6470613a3a507265636f6d6d69740a3c482c204e3e2c20417574686f726974795369676e61747572652c3e00010000e101084066696e616c6974795f6772616e6470613045717569766f636174696f6e0c0849640188045601e501045301e90100100130726f756e645f6e756d62657218010c7536340001206964656e7469747988010849640001146669727374f101011828562c2053290001187365636f6e64f101011828562c2053290000e501084066696e616c6974795f6772616e6470611c507265766f74650804480134044e01100008012c7461726765745f68617368340104480001347461726765745f6e756d6265721001044e0000e9010c5073705f636f6e73656e7375735f6772616e6470610c617070245369676e617475726500000400ed010148656432353531393a3a5369676e61747572650000ed01000003400000000800f10100000408e501e90100f501084066696e616c6974795f6772616e6470613045717569766f636174696f6e0c0849640188045601f901045301e90100100130726f756e645f6e756d62657218010c7536340001206964656e7469747988010849640001146669727374fd01011828562c2053290001187365636f6e64fd01011828562c2053290000f901084066696e616c6974795f6772616e64706124507265636f6d6d69740804480134044e01100008012c7461726765745f68617368340104480001347461726765745f6e756d6265721001044e0000fd0100000408f901e901000102081c73705f636f726510566f69640001000005020c3870616c6c65745f6772616e6470611870616c6c6574144572726f7204045400011c2c50617573654661696c65640000080501417474656d707420746f207369676e616c204752414e445041207061757365207768656e2074686520617574686f72697479207365742069736e2774206c697665a42865697468657220706175736564206f7220616c72656164792070656e64696e67207061757365292e30526573756d654661696c65640001081101417474656d707420746f207369676e616c204752414e44504120726573756d65207768656e2074686520617574686f72697479207365742069736e277420706175736564a028656974686572206c697665206f7220616c72656164792070656e64696e6720726573756d65292e344368616e676550656e64696e67000204e8417474656d707420746f207369676e616c204752414e445041206368616e67652077697468206f6e6520616c72656164792070656e64696e672e1c546f6f536f6f6e000304bc43616e6e6f74207369676e616c20666f72636564206368616e676520736f20736f6f6e206166746572206c6173742e60496e76616c69644b65794f776e65727368697050726f6f66000404310141206b6579206f776e6572736869702070726f6f662070726f76696465642061732070617274206f6620616e2065717569766f636174696f6e207265706f727420697320696e76616c69642e60496e76616c696445717569766f636174696f6e50726f6f660005043101416e2065717569766f636174696f6e2070726f6f662070726f76696465642061732070617274206f6620616e2065717569766f636174696f6e207265706f727420697320696e76616c69642e584475706c69636174654f6666656e63655265706f727400060415014120676976656e2065717569766f636174696f6e207265706f72742069732076616c69642062757420616c72656164792070726576696f75736c79207265706f727465642e048054686520604572726f726020656e756d206f6620746869732070616c6c65742e09020c4c626f756e6465645f636f6c6c656374696f6e73407765616b5f626f756e6465645f766563385765616b426f756e646564566563080454010d02045300000400150201185665633c543e00000d020c3c70616c6c65745f62616c616e6365731474797065732c42616c616e63654c6f636b041c42616c616e63650118000c01086964a50101384c6f636b4964656e746966696572000118616d6f756e7418011c42616c616e636500011c726561736f6e731102011c526561736f6e73000011020c3c70616c6c65745f62616c616e6365731474797065731c526561736f6e7300010c0c466565000000104d6973630001000c416c6c0002000015020000020d020019020c4c626f756e6465645f636f6c6c656374696f6e732c626f756e6465645f76656328426f756e646564566563080454011d02045300000400210201185665633c543e00001d020c3c70616c6c65745f62616c616e6365731474797065732c52657365727665446174610844526573657276654964656e74696669657201a5011c42616c616e63650118000801086964a5010144526573657276654964656e746966696572000118616d6f756e7418011c42616c616e6365000021020000021d020025020c4c626f756e6465645f636f6c6c656374696f6e732c626f756e6465645f76656328426f756e6465645665630804540129020453000004003d0201185665633c543e0000290214346672616d655f737570706f72741874726169747318746f6b656e73106d697363204964416d6f756e7408084964012d021c42616c616e636501180008010869642d0201084964000118616d6f756e7418011c42616c616e636500002d0208586e6f64655f73756274656e736f725f72756e74696d654452756e74696d65486f6c64526561736f6e00010c20507265696d61676504003102016c70616c6c65745f707265696d6167653a3a486f6c64526561736f6e000e0020526567697374727904003502016c70616c6c65745f72656769737472793a3a486f6c64526561736f6e00110020536166654d6f646504003902017070616c6c65745f736166655f6d6f64653a3a486f6c64526561736f6e0014000031020c3c70616c6c65745f707265696d6167651870616c6c657428486f6c64526561736f6e00010420507265696d6167650000000035020c3c70616c6c65745f72656769737472791870616c6c657428486f6c64526561736f6e0001044052656769737472794964656e746974790000000039020c4070616c6c65745f736166655f6d6f64651870616c6c657428486f6c64526561736f6e00010434456e7465724f72457874656e64000000003d0200000229020041020c4c626f756e6465645f636f6c6c656374696f6e732c626f756e6465645f76656328426f756e6465645665630804540145020453000004004d0201185665633c543e0000450214346672616d655f737570706f72741874726169747318746f6b656e73106d697363204964416d6f756e74080849640149021c42616c616e63650118000801086964490201084964000118616d6f756e7418011c42616c616e63650000490208586e6f64655f73756274656e736f725f72756e74696d654c52756e74696d65467265657a65526561736f6e000100004d0200000245020051020c3c70616c6c65745f62616c616e6365731870616c6c65741043616c6c080454000449000124507472616e736665725f616c6c6f775f646561746808011064657374550201504163636f756e7449644c6f6f6b75704f663c543e00011476616c7565300128543a3a42616c616e636500001cd45472616e7366657220736f6d65206c697175696420667265652062616c616e636520746f20616e6f74686572206163636f756e742e003501607472616e736665725f616c6c6f775f6465617468602077696c6c207365742074686520604672656542616c616e636560206f66207468652073656e64657220616e642072656365697665722e11014966207468652073656e6465722773206163636f756e742069732062656c6f7720746865206578697374656e7469616c206465706f736974206173206120726573756c74b06f6620746865207472616e736665722c20746865206163636f756e742077696c6c206265207265617065642e001501546865206469737061746368206f726967696e20666f7220746869732063616c6c206d75737420626520605369676e65646020627920746865207472616e736163746f722e38666f7263655f7472616e736665720c0118736f75726365550201504163636f756e7449644c6f6f6b75704f663c543e00011064657374550201504163636f756e7449644c6f6f6b75704f663c543e00011476616c7565300128543a3a42616c616e6365000208610145786163746c7920617320607472616e736665725f616c6c6f775f6465617468602c2065786365707420746865206f726967696e206d75737420626520726f6f7420616e642074686520736f75726365206163636f756e74446d6179206265207370656369666965642e4c7472616e736665725f6b6565705f616c69766508011064657374550201504163636f756e7449644c6f6f6b75704f663c543e00011476616c7565300128543a3a42616c616e6365000318590153616d6520617320746865205b607472616e736665725f616c6c6f775f6465617468605d2063616c6c2c206275742077697468206120636865636b207468617420746865207472616e736665722077696c6c206e6f74606b696c6c20746865206f726967696e206163636f756e742e00e8393925206f66207468652074696d6520796f752077616e74205b607472616e736665725f616c6c6f775f6465617468605d20696e73746561642e00f05b607472616e736665725f616c6c6f775f6465617468605d3a207374727563742e50616c6c65742e68746d6c236d6574686f642e7472616e73666572307472616e736665725f616c6c08011064657374550201504163636f756e7449644c6f6f6b75704f663c543e0001286b6565705f616c697665240110626f6f6c00043c05015472616e736665722074686520656e74697265207472616e7366657261626c652062616c616e63652066726f6d207468652063616c6c6572206163636f756e742e0059014e4f54453a20546869732066756e6374696f6e206f6e6c7920617474656d70747320746f207472616e73666572205f7472616e7366657261626c655f2062616c616e6365732e2054686973206d65616e7320746861746101616e79206c6f636b65642c2072657365727665642c206f72206578697374656e7469616c206465706f7369747320287768656e20606b6565705f616c6976656020697320607472756560292c2077696c6c206e6f742062655d017472616e7366657272656420627920746869732066756e6374696f6e2e20546f20656e73757265207468617420746869732066756e6374696f6e20726573756c747320696e2061206b696c6c6564206163636f756e742c4501796f75206d69676874206e65656420746f207072657061726520746865206163636f756e742062792072656d6f76696e6720616e79207265666572656e636520636f756e746572732c2073746f72616765406465706f736974732c206574632e2e2e00c0546865206469737061746368206f726967696e206f6620746869732063616c6c206d757374206265205369676e65642e00a02d206064657374603a2054686520726563697069656e74206f6620746865207472616e736665722e59012d20606b6565705f616c697665603a204120626f6f6c65616e20746f2064657465726d696e652069662074686520607472616e736665725f616c6c60206f7065726174696f6e2073686f756c642073656e6420616c6c4d0120206f66207468652066756e647320746865206163636f756e74206861732c2063617573696e67207468652073656e646572206163636f756e7420746f206265206b696c6c6564202866616c7365292c206f72590120207472616e736665722065766572797468696e6720657863657074206174206c6561737420746865206578697374656e7469616c206465706f7369742c2077686963682077696c6c2067756172616e74656520746f9c20206b656570207468652073656e646572206163636f756e7420616c697665202874727565292e3c666f7263655f756e7265736572766508010c77686f550201504163636f756e7449644c6f6f6b75704f663c543e000118616d6f756e74180128543a3a42616c616e636500050cb0556e7265736572766520736f6d652062616c616e63652066726f6d2061207573657220627920666f7263652e006c43616e206f6e6c792062652063616c6c656420627920524f4f542e40757067726164655f6163636f756e747304010c77686f5d0201445665633c543a3a4163636f756e7449643e0006207055706772616465206120737065636966696564206163636f756e742e00742d20606f726967696e603a204d75737420626520605369676e6564602e902d206077686f603a20546865206163636f756e7420746f2062652075706772616465642e005501546869732077696c6c20776169766520746865207472616e73616374696f6e20666565206966206174206c6561737420616c6c2062757420313025206f6620746865206163636f756e7473206e656564656420746f410162652075706772616465642e20285765206c657420736f6d65206e6f74206861766520746f206265207570677261646564206a75737420696e206f7264657220746f20616c6c6f7720666f722074686558706f73736962696c697479206f6620636875726e292e44666f7263655f7365745f62616c616e636508010c77686f550201504163636f756e7449644c6f6f6b75704f663c543e0001206e65775f66726565300128543a3a42616c616e636500080cac5365742074686520726567756c61722062616c616e6365206f66206120676976656e206163636f756e742e00b0546865206469737061746368206f726967696e20666f7220746869732063616c6c2069732060726f6f74602e6c666f7263655f61646a7573745f746f74616c5f69737375616e6365080124646972656374696f6e6102014c41646a7573746d656e74446972656374696f6e00011464656c7461300128543a3a42616c616e6365000914b841646a7573742074686520746f74616c2069737375616e636520696e20612073617475726174696e67207761792e00fc43616e206f6e6c792062652063616c6c656420627920726f6f7420616e6420616c77617973206e65656473206120706f736974697665206064656c7461602e002423204578616d706c65106275726e08011476616c7565300128543a3a42616c616e63650001286b6565705f616c697665240110626f6f6c000a1cfc4275726e2074686520737065636966696564206c697175696420667265652062616c616e63652066726f6d20746865206f726967696e206163636f756e742e002501496620746865206f726967696e2773206163636f756e7420656e64732075702062656c6f7720746865206578697374656e7469616c206465706f736974206173206120726573756c7409016f6620746865206275726e20616e6420606b6565705f616c697665602069732066616c73652c20746865206163636f756e742077696c6c206265207265617065642e005101556e6c696b652073656e64696e672066756e647320746f2061205f6275726e5f20616464726573732c207768696368206d6572656c79206d616b6573207468652066756e647320696e61636365737369626c652c21017468697320606275726e60206f7065726174696f6e2077696c6c2072656475636520746f74616c2069737375616e63652062792074686520616d6f756e74205f6275726e65645f2e040d01436f6e7461696e7320612076617269616e742070657220646973706174636861626c652065787472696e736963207468617420746869732070616c6c6574206861732e55020c2873705f72756e74696d65306d756c746961646472657373304d756c74694164647265737308244163636f756e7449640100304163636f756e74496e64657801a4011408496404000001244163636f756e74496400000014496e6465780400590201304163636f756e74496e6465780001000c526177040038011c5665633c75383e0002002441646472657373333204000401205b75383b2033325d000300244164647265737332300400110101205b75383b2032305d000400005902000006a4005d02000002000061020c3c70616c6c65745f62616c616e6365731474797065734c41646a7573746d656e74446972656374696f6e00010820496e6372656173650000002044656372656173650001000065020c3c70616c6c65745f62616c616e6365731870616c6c6574144572726f720804540004490001303856657374696e6742616c616e63650000049c56657374696e672062616c616e636520746f6f206869676820746f2073656e642076616c75652e544c69717569646974795265737472696374696f6e73000104c84163636f756e74206c6971756964697479207265737472696374696f6e732070726576656e74207769746864726177616c2e4c496e73756666696369656e7442616c616e63650002047842616c616e636520746f6f206c6f7720746f2073656e642076616c75652e484578697374656e7469616c4465706f736974000304ec56616c756520746f6f206c6f7720746f20637265617465206163636f756e742064756520746f206578697374656e7469616c206465706f7369742e34457870656e646162696c697479000404905472616e736665722f7061796d656e7420776f756c64206b696c6c206163636f756e742e5c4578697374696e6756657374696e675363686564756c65000504cc412076657374696e67207363686564756c6520616c72656164792065786973747320666f722074686973206163636f756e742e2c446561644163636f756e740006048c42656e6566696369617279206163636f756e74206d757374207072652d65786973742e3c546f6f4d616e795265736572766573000704b84e756d626572206f66206e616d65642072657365727665732065786365656420604d61785265736572766573602e30546f6f4d616e79486f6c6473000804f84e756d626572206f6620686f6c647320657863656564206056617269616e74436f756e744f663c543a3a52756e74696d65486f6c64526561736f6e3e602e38546f6f4d616e79467265657a6573000904984e756d626572206f6620667265657a65732065786365656420604d6178467265657a6573602e4c49737375616e63654465616374697661746564000a0401015468652069737375616e63652063616e6e6f74206265206d6f6469666965642073696e636520697420697320616c72656164792064656163746976617465642e2444656c74615a65726f000b04645468652064656c74612063616e6e6f74206265207a65726f2e048054686520604572726f726020656e756d206f6620746869732070616c6c65742e69020c3473705f61726974686d657469632c66697865645f706f696e7424466978656455313238000004002001107531323800006d02086870616c6c65745f7472616e73616374696f6e5f7061796d656e742052656c6561736573000108245631416e6369656e7400000008563200010000710200000408009c007502000004089c0000790200000408ac18007d020000040c00009c008102000004080000008502083c7375627374726174655f666978656424466978656455313238041046726163018902000401106269747320011075313238000089020c1c747970656e756d1075696e741055496e74080455018d02044201ad020008010c6d73628d0201045500010c6c7362ad0201044200008d020c1c747970656e756d1075696e741055496e74080455019102044201ad020008010c6d7362910201045500010c6c7362ad02010442000091020c1c747970656e756d1075696e741055496e74080455019502044201ad020008010c6d7362950201045500010c6c7362ad02010442000095020c1c747970656e756d1075696e741055496e74080455019902044201ad020008010c6d7362990201045500010c6c7362ad02010442000099020c1c747970656e756d1075696e741055496e74080455019d02044201ad020008010c6d73629d0201045500010c6c7362ad0201044200009d020c1c747970656e756d1075696e741055496e7408045501a102044201ad020008010c6d7362a10201045500010c6c7362ad020104420000a1020c1c747970656e756d1075696e741055496e7408045501a502044201a9020008010c6d7362a50201045500010c6c7362a9020104420000a5020c1c747970656e756d1075696e7414555465726d00000000a9020c1c747970656e756d0c62697408423100000000ad020c1c747970656e756d0c62697408423000000000b102000004089c9c00b5020000029c00b902000002bd0200bd020000040c00181800c1020000022400c502000002b10200c9020c4070616c6c65745f73756274656e736f721870616c6c65742041786f6e496e666f0000200114626c6f636b18010c75363400011c76657273696f6e10010c753332000108697020011075313238000110706f72749c010c75313600011c69705f74797065080108753800012070726f746f636f6c0801087538000130706c616365686f6c646572310801087538000130706c616365686f6c6465723208010875380000cd020c4070616c6c65745f73756274656e736f721870616c6c6574444e6575726f6e436572746966696361746500000801287075626c69635f6b6579d1020170426f756e6465645665633c75382c20436f6e73745533323c36343e3e000124616c676f726974686d08010875380000d1020c4c626f756e6465645f636f6c6c656374696f6e732c626f756e6465645f76656328426f756e64656456656308045401080453000004003801185665633c543e0000d5020c4070616c6c65745f73756274656e736f721870616c6c65743850726f6d657468657573496e666f0000140114626c6f636b18010c75363400011c76657273696f6e10010c753332000108697020011075313238000110706f72749c010c75313600011c69705f7479706508010875380000d9020c4070616c6c65745f73756274656e736f721870616c6c657434436861696e4964656e7469747900001801106e616d6538011c5665633c75383e00010c75726c38011c5665633c75383e000114696d61676538011c5665633c75383e00011c646973636f726438011c5665633c75383e00012c6465736372697074696f6e38011c5665633c75383e0001286164646974696f6e616c38011c5665633c75383e0000dd020c4070616c6c65745f73756274656e736f721870616c6c6574385375626e65744964656e7469747900000c012c7375626e65745f6e616d6538011c5665633c75383e00012c6769746875625f7265706f38011c5665633c75383e0001387375626e65745f636f6e7461637438011c5665633c75383e0000e1020000040c009c9c00e502000002e90200e902000004103418181800ed02000004089c1800f102000002f50200f5020000040c00f9021800f9020c4c626f756e6465645f636f6c6c656374696f6e732c626f756e6465645f76656328426f756e64656456656308045401080453000004003801185665633c543e0000fd020c4070616c6c65745f73756274656e736f721870616c6c65741043616c6c0404540001ac2c7365745f776569676874731001186e65747569649c010c7531360001146465737473b50201205665633c7531363e00011c77656967687473b50201205665633c7531363e00012c76657273696f6e5f6b657918010c7536340000e821012d2d2d2053657473207468652063616c6c6572207765696768747320666f722074686520696e63656e74697665206d656368616e69736d2e205468652063616c6c2063616e20626531016d6164652066726f6d2074686520686f746b6579206163636f756e7420736f20697320706f74656e7469616c6c7920696e7365637572652c20686f77657665722c207468652064616d61676539016f66206368616e67696e672077656967687473206973206d696e696d616c20696620636175676874206561726c792e20546869732066756e6374696f6e20696e636c7564657320616c6c207468654d01636865636b73207468617420746865207061737365642077656967687473206d6565742074686520726571756972656d656e74732e2053746f7265642061732075313673207468657920726570726573656e742d01726174696f6e616c2076616c75657320696e207468652072616e6765205b302c315d2077686963682073756d20746f203120616e642063616e20626520696e746572707265746564206173390170726f626162696c69746965732e2054686520737065636966696320776569676874732064657465726d696e6520686f7720696e666c6174696f6e2070726f70616761746573206f7574776172643c66726f6d207468697320706565722e0019014e6f74653a205468652031362062697420696e74656765727320776569676874732073686f756c6420726570726573656e7420312e3020617320746865206d6178207531362e8901486f77657665722c207468652066756e6374696f6e206e6f726d616c697a657320616c6c20696e74656765727320746f207531365f6d617820616e797761792e2054686973206d65616e732074686174206966207468652073756d206f6620616c6c4501656c656d656e7473206973206c6172676572206f7220736d616c6c6572207468616e2074686520616d6f756e74206f6620656c656d656e7473202a207531365f6d61782c20616c6c20656c656d656e74739477696c6c20626520636f7272656374656420666f72207468697320646576696174696f6e2e001c2320417267733ac02a20606f726967696e603a20283c54206173206672616d655f73797374656d3a3a436f6e6669673e4f726967696e293aec202020202d205468652063616c6c65722c206120686f746b65792077686f2077697368657320746f2073657420746865697220776569676874732e00442a20606e6574756964602028753136293acc092d20546865206e6574776f726b20756964207765206172652073657474696e672074686573652077656967687473206f6e2e00542a206064657374736020285665633c7531363e293ad4092d20546865206564676520656e64706f696e7420666f7220746865207765696768742c20692e652e206a20666f7220775f696a2e005c2a2027776569676874732720285665633c7531363e293aec092d205468652075313620696e746567657220656e636f64656420776569676874732e20496e74657270726574656420617320726174696f6e616ce0090976616c75657320696e207468652072616e6765205b302c315d2e2054686579206d7573742073756d20746f20696e33323a3a4d41582e00602a202776657273696f6e5f6b65792720282075363420293a0d01092d20546865206e6574776f726b2076657273696f6e206b657920746f20636865636b206966207468652076616c696461746f7220697320757020746f20646174652e002023204576656e743a342a20576569676874735365743bc0092d204f6e207375636365737366756c6c792073657474696e67207468652077656967687473206f6e20636861696e2e002423205261697365733a6c2a20275375624e6574776f726b446f65734e6f744578697374273adc092d20417474656d7074696e6720746f207365742077656967687473206f6e2061206e6f6e2d6578697374656e74206e6574776f726b2e00482a20274e6f7452656769737465726564273aec092d20417474656d7074696e6720746f2073657420776569676874732066726f6d2061206e6f6e2072656769737465726564206163636f756e742e00682a20275765696768745665634e6f74457175616c53697a65273ae8092d20417474656d7074696e6720746f20736574207765696768747320776974682075696473206e6f74206f662073616d65206c656e6774682e00482a20274475706c696361746555696473273ac4092d20417474656d7074696e6720746f2073657420776569676874732077697468206475706c696361746520756964732e0094202020202a2027556964734c656e67746845786365656455696473496e5375624e6574273ae0092d20417474656d7074696e6720746f2073657420776569676874732061626f766520746865206d617820616c6c6f77656420756964732e00702a2027556964566563436f6e7461696e496e76616c69644f6e65273abc092d20417474656d7074696e6720746f207365742077656967687473207769746820696e76616c696420756964732e00642a20275765696768745665634c656e67746849734c6f77273ae4092d20417474656d7074696e6720746f20736574207765696768747320776974682066657765722077656967687473207468616e206d696e2e00582a20274d61785765696768744578636565646564273af0092d20417474656d7074696e6720746f2073657420776569676874732077697468206d61782076616c756520657863656564696e67206c696d69742e4462617463685f7365745f776569676874730c011c6e657475696473b801445665633c436f6d706163743c7531363e3e00011c77656967687473010301985665633c5665633c28436f6d706163743c7531363e2c20436f6d706163743c7531363e293e3e00013076657273696f6e5f6b6579730d0301445665633c436f6d706163743c7536343e3e0050640d012d2d2d20416c6c6f7773206120686f746b657920746f20736574207765696768747320666f72206d756c7469706c65206e65747569647320617320612062617463682e001c2320417267733ac02a20606f726967696e603a20283c54206173206672616d655f73797374656d3a3a436f6e6669673e4f726967696e293aec202020202d205468652063616c6c65722c206120686f746b65792077686f2077697368657320746f2073657420746865697220776569676874732e00802a20606e6574756964736020285665633c436f6d706163743c7531363e3e293ad0092d20546865206e6574776f726b2075696473207765206172652073657474696e672074686573652077656967687473206f6e2e00d02a2060776569676874736020285665633c5665633c28436f6d706163743c7531363e2c20436f6d706163743c7531363e293e293af0092d20546865207765696768747320746f2073657420666f722065616368206e6574776f726b2e205b287569642c20776569676874292c202e2e2e5d00942a206076657273696f6e5f6b6579736020285665633c436f6d706163743c7536343e3e293a1101092d20546865206e6574776f726b2076657273696f6e206b65797320746f20636865636b206966207468652076616c696461746f7220697320757020746f20646174652e002023204576656e743a342a20576569676874735365743bc0092d204f6e207375636365737366756c6c792073657474696e67207468652077656967687473206f6e20636861696e2e602a20426174636857656967687473436f6d706c657465643b6c092d204f6e2073756363657373206f66207468652062617463682e6c2a204261746368436f6d706c65746564576974684572726f72733bc4092d204f6e206661696c757265206f6620616e79206f6620746865207765696768747320696e207468652062617463682e602a2042617463685765696768744974656d4661696c65643bc0092d204f6e206661696c75726520666f722065616368206661696c6564206974656d20696e207468652062617463682e0038636f6d6d69745f776569676874730801186e65747569649c010c75313600012c636f6d6d69745f686173683401104832353600604c19012d2d2d2d205573656420746f20636f6d6d697420612068617368206f6620796f7572207765696768742076616c75657320746f206c617465722062652072657665616c65642e001c2320417267733aec2a20606f726967696e603a2028603c54206173206672616d655f73797374656d3a3a436f6e6669673e3a3a52756e74696d654f726967696e60293aac20202d20546865207369676e6174757265206f662074686520636f6d6d697474696e6720686f746b65792e004c2a20606e65747569646020286075313660293a7c20202d2054686520753136206e6574776f726b206964656e7469666965722e00642a2060636f6d6d69745f68617368602028604832353660293ac020202d20546865206861736820726570726573656e74696e672074686520636f6d6d697474656420776569676874732e002423205261697365733a642a2060436f6d6d697452657665616c44697361626c6564603a190120202d20417474656d7074696e6720746f20636f6d6d6974207768656e2074686520636f6d6d69742d72657665616c206d656368616e69736d2069732064697361626c65642e00742a2060546f6f4d616e79556e72657665616c6564436f6d6d697473603a750120202d20417474656d7074696e6720746f20636f6d6d6974207768656e20746865207573657220686173206d6f7265207468616e2074686520616c6c6f776564206c696d6974206f6620756e72657665616c656420636f6d6d6974732e005062617463685f636f6d6d69745f7765696768747308011c6e657475696473b801445665633c436f6d706163743c7531363e3e000134636f6d6d69745f686173686573b401245665633c483235363e00645831012d2d2d20416c6c6f7773206120686f746b657920746f20636f6d6d6974207765696768742068617368657320666f72206d756c7469706c65206e65747569647320617320612062617463682e001c2320417267733ac02a20606f726967696e603a20283c54206173206672616d655f73797374656d3a3a436f6e6669673e4f726967696e293aec202020202d205468652063616c6c65722c206120686f746b65792077686f2077697368657320746f2073657420746865697220776569676874732e00802a20606e6574756964736020285665633c436f6d706163743c7531363e3e293ad0092d20546865206e6574776f726b2075696473207765206172652073657474696e672074686573652077656967687473206f6e2e00782a2060636f6d6d69745f6861736865736020285665633c483235363e293a7c092d2054686520636f6d6d69742068617368657320746f20636f6d6d69742e002023204576656e743a342a20576569676874735365743bc0092d204f6e207375636365737366756c6c792073657474696e67207468652077656967687473206f6e20636861696e2e602a20426174636857656967687473436f6d706c657465643b6c092d204f6e2073756363657373206f66207468652062617463682e6c2a204261746368436f6d706c65746564576974684572726f72733bc4092d204f6e206661696c757265206f6620616e79206f6620746865207765696768747320696e207468652062617463682e602a2042617463685765696768744974656d4661696c65643bc0092d204f6e206661696c75726520666f722065616368206661696c6564206974656d20696e207468652062617463682e003872657665616c5f776569676874731401186e65747569649c010c75313600011075696473b50201205665633c7531363e00011876616c756573b50201205665633c7531363e00011073616c74b50201205665633c7531363e00012c76657273696f6e5f6b657918010c75363400619401012d2d2d2d205573656420746f2072657665616c20746865207765696768747320666f7220612070726576696f75736c7920636f6d6d697474656420686173682e001c2320417267733aec2a20606f726967696e603a2028603c54206173206672616d655f73797374656d3a3a436f6e6669673e3a3a52756e74696d654f726967696e60293aa820202d20546865207369676e6174757265206f66207468652072657665616c696e6720686f746b65792e004c2a20606e65747569646020286075313660293a7c20202d2054686520753136206e6574776f726b206964656e7469666965722e00582a206075696473602028605665633c7531363e60293ab020202d20546865207569647320666f72207468652077656967687473206265696e672072657665616c65642e00602a206076616c756573602028605665633c7531363e60293ab420202d205468652076616c756573206f66207468652077656967687473206265696e672072657665616c65642e00582a206073616c74602028605665633c7531363e60293ab820202d205468652073616c74207573656420746f2067656e65726174652074686520636f6d6d697420686173682e00602a206076657273696f6e5f6b65796020286075363460293a7020202d20546865206e6574776f726b2076657273696f6e206b65792e002423205261697365733a642a2060436f6d6d697452657665616c44697361626c6564603a390120202d20417474656d7074696e6720746f2072657665616c2077656967687473207768656e2074686520636f6d6d69742d72657665616c206d656368616e69736d2069732064697361626c65642e00642a20604e6f57656967687473436f6d6d6974466f756e64603af020202d20417474656d7074696e6720746f2072657665616c207765696768747320776974686f757420616e206578697374696e6720636f6d6d69742e00602a206045787069726564576569676874436f6d6d6974603ae820202d20417474656d7074696e6720746f2072657665616c20612077656967687420636f6d6d697420746861742068617320657870697265642e004c2a206052657665616c546f6f4561726c79603a050120202d20417474656d7074696e6720746f2072657665616c2077656967687473206f757473696465207468652076616c69642072657665616c20706572696f642e00902a2060496e76616c696452657665616c436f6d6d6974486173684e6f744d61746368603ae020202d205468652072657665616c6564206861736820646f6573206e6f74206d6174636820616e7920636f6d6d697474656420686173682e004c636f6d6d69745f637276335f776569676874730c01186e65747569649c010c753136000118636f6d6d6974f90201d0426f756e6465645665633c75382c20436f6e73745533323c4d41585f435256335f434f4d4d49545f53495a455f42595445533e3e00013072657665616c5f726f756e6418010c75363400637449012d2d2d2d205573656420746f20636f6d6d697420656e6372797074656420636f6d6d69742d72657665616c207633207765696768742076616c75657320746f206c617465722062652072657665616c65642e001c2320417267733aec2a20606f726967696e603a2028603c54206173206672616d655f73797374656d3a3a436f6e6669673e3a3a52756e74696d654f726967696e60293a6820202d2054686520636f6d6d697474696e6720686f746b65792e004c2a20606e65747569646020286075313660293a7c20202d2054686520753136206e6574776f726b206964656e7469666965722e005c2a2060636f6d6d6974602028605665633c75383e60293a9020202d2054686520656e6372797074656420636f6d7072657373656420636f6d6d69742e6c2020202054686520737465707320666f722074686973206172653aa820202020312e20496e7374616e7469617465205b6057656967687473546c6f636b5061796c6f6164605d010120202020322e2053657269616c697a65206974207573696e672074686520607061726974795f7363616c655f636f6465633a3a456e636f646560207472616974750220202020332e20456e637279707420697420666f6c6c6f77696e6720746865207374657073202868657265295b68747470733a2f2f6769746875622e636f6d2f696465616c2d6c6162352f746c652f626c6f622f663865363031396630666230326333383065626661366233306566623631373836646564653037622f74696d656c6f636b2f7372632f746c6f636b2e7273234c3238332d4c3333365ddc20202020202020746f2070726f647563652061205b60544c45436970686572746578743c54696e79424c533338313e605d20747970652e4d0120202020342e2053657269616c697a6520616e6420636f6d7072657373207573696e6720746865206061726b2d73657269616c697a6560206043616e6f6e6963616c53657269616c697a65602074726169742e005c2a2072657665616c5f726f756e6420286075363460293a5d012020202d20546865206472616e642072657665616c20726f756e642077686963682077696c6c206265206176616c6961626c6520647572696e672065706f636820606e2b31602066726f6d207468652063757272656e742c202020202065706f63682e002423205261697365733a6c2a2060436f6d6d697452657665616c563344697361626c6564603a190120202d20417474656d7074696e6720746f20636f6d6d6974207768656e2074686520636f6d6d69742d72657665616c206d656368616e69736d2069732064697361626c65642e00742a2060546f6f4d616e79556e72657665616c6564436f6d6d697473603a750120202d20417474656d7074696e6720746f20636f6d6d6974207768656e20746865207573657220686173206d6f7265207468616e2074686520616c6c6f776564206c696d6974206f6620756e72657665616c656420636f6d6d6974732e005062617463685f72657665616c5f776569676874731401186e65747569649c010c753136000124756964735f6c697374110301345665633c5665633c7531363e3e00012c76616c7565735f6c697374110301345665633c5665633c7531363e3e00012873616c74735f6c697374110301345665633c5665633c7531363e3e00013076657273696f6e5f6b657973510101205665633c7536343e00629cf82d2d2d2d2054686520696d706c656d656e746174696f6e20666f722062617463682072657665616c696e6720636f6d6d697474656420776569676874732e001c2320417267733aec2a20606f726967696e603a2028603c54206173206672616d655f73797374656d3a3a436f6e6669673e3a3a52756e74696d654f726967696e60293aa820202d20546865207369676e6174757265206f66207468652072657665616c696e6720686f746b65792e004c2a20606e65747569646020286075313660293a7c20202d2054686520753136206e6574776f726b206964656e7469666965722e00802a2060756964735f6c697374602028605665633c5665633c7531363e3e60293ae820202d2041206c697374206f66207569647320666f72206561636820736574206f662077656967687473206265696e672072657665616c65642e00882a206076616c7565735f6c697374602028605665633c5665633c7531363e3e60293af020202d2041206c697374206f662076616c75657320666f72206561636820736574206f662077656967687473206265696e672072657665616c65642e00842a206073616c74735f6c697374602028605665633c5665633c7531363e3e60293adc20202d2041206c697374206f662073616c7473207573656420746f2067656e65726174652074686520636f6d6d6974206861736865732e00782a206076657273696f6e5f6b657973602028605665633c7536343e60293a8c20202d2041206c697374206f66206e6574776f726b2076657273696f6e206b6579732e002423205261697365733a642a2060436f6d6d697452657665616c44697361626c6564603a390120202d20417474656d7074696e6720746f2072657665616c2077656967687473207768656e2074686520636f6d6d69742d72657665616c206d656368616e69736d2069732064697361626c65642e00642a20604e6f57656967687473436f6d6d6974466f756e64603af020202d20417474656d7074696e6720746f2072657665616c207765696768747320776974686f757420616e206578697374696e6720636f6d6d69742e00602a206045787069726564576569676874436f6d6d6974603ae820202d20417474656d7074696e6720746f2072657665616c20612077656967687420636f6d6d697420746861742068617320657870697265642e004c2a206052657665616c546f6f4561726c79603a050120202d20417474656d7074696e6720746f2072657665616c2077656967687473206f757473696465207468652076616c69642072657665616c20706572696f642e00902a2060496e76616c696452657665616c436f6d6d6974486173684e6f744d61746368603ae020202d205468652072657665616c6564206861736820646f6573206e6f74206d6174636820616e7920636f6d6d697474656420686173682e00602a2060496e76616c6964496e7075744c656e67746873603ac020202d2054686520696e70757420766563746f727320617265206f66206d69736d617463686564206c656e677468732e3c7365745f74616f5f776569676874731401186e65747569649c010c753136000118686f746b6579000130543a3a4163636f756e7449640001146465737473b50201205665633c7531363e00011c77656967687473b50201205665633c7531363e00012c76657273696f6e5f6b657918010c7536340008f01c2320417267733ac02a20606f726967696e603a20283c54206173206672616d655f73797374656d3a3a436f6e6669673e4f726967696e293ae0092d205468652063616c6c65722c206120686f746b65792077686f2077697368657320746f2073657420746865697220776569676874732e00442a20606e6574756964602028753136293acc092d20546865206e6574776f726b20756964207765206172652073657474696e672074686573652077656967687473206f6e2e00682a2060686f746b6579602028543a3a4163636f756e744964293a1101092d2054686520686f746b6579206173736f636961746564207769746820746865206f7065726174696f6e20616e64207468652063616c6c696e6720636f6c646b65792e00542a206064657374736020285665633c7531363e293ad4092d20546865206564676520656e64706f696e7420666f7220746865207765696768742c20692e652e206a20666f7220775f696a2e005c2a2027776569676874732720285665633c7531363e293aec092d205468652075313620696e746567657220656e636f64656420776569676874732e20496e74657270726574656420617320726174696f6e616ce0090976616c75657320696e207468652072616e6765205b302c315d2e2054686579206d7573742073756d20746f20696e33323a3a4d41582e00602a202776657273696f6e5f6b65792720282075363420293a0d01092d20546865206e6574776f726b2076657273696f6e206b657920746f20636865636b206966207468652076616c696461746f7220697320757020746f20646174652e002023204576656e743a00342a20576569676874735365743bc0092d204f6e207375636365737366756c6c792073657474696e67207468652077656967687473206f6e20636861696e2e002423205261697365733a005c2a204e6f6e4173736f636961746564436f6c644b65793be8092d20417474656d7074696e6720746f207365742077656967687473206f6e2061206e6f6e2d6173736f63696174656420636f6c64206b65792e006c2a20275375624e6574776f726b446f65734e6f744578697374273adc092d20417474656d7074696e6720746f207365742077656967687473206f6e2061206e6f6e2d6578697374656e74206e6574776f726b2e00482a20274e6f74526f6f745375626e6574273a1901092d20417474656d7074696e6720746f207365742077656967687473206f6e2061207375626e65742074686174206973206e6f742074686520726f6f74206e6574776f726b2e00682a20275765696768745665634e6f74457175616c53697a65273ae8092d20417474656d7074696e6720746f20736574207765696768747320776974682075696473206e6f74206f662073616d65206c656e6774682e00702a2027556964566563436f6e7461696e496e76616c69644f6e65273abc092d20417474656d7074696e6720746f207365742077656967687473207769746820696e76616c696420756964732e00482a20274e6f7452656769737465726564273aec092d20417474656d7074696e6720746f2073657420776569676874732066726f6d2061206e6f6e2072656769737465726564206163636f756e742e00642a20275765696768745665634c656e67746849734c6f77273ae4092d20417474656d7074696e6720746f20736574207765696768747320776974682066657765722077656967687473207468616e206d696e2e007c202a2027496e636f727265637457656967687456657273696f6e4b6579273a210120202020202d20417474656d7074696e6720746f20736574207765696768747320776974682074686520696e636f7272656374206e6574776f726b2076657273696f6e206b65792e006c202a202753657474696e6757656967687473546f6f46617374273aa820202020202d20417474656d7074696e6720746f20736574207765696768747320746f6f20666173742e00642a20275765696768745665634c656e67746849734c6f77273ae4092d20417474656d7074696e6720746f20736574207765696768747320776974682066657765722077656967687473207468616e206d696e2e00582a20274d61785765696768744578636565646564273af0092d20417474656d7074696e6720746f2073657420776569676874732077697468206d61782076616c756520657863656564696e67206c696d69742e003c6265636f6d655f64656c6567617465040118686f746b6579000130543a3a4163636f756e74496400015c7c2d2d2d205365747320746865206b657920617320612064656c65676174652e001c2320417267733ac02a20276f726967696e273a20283c54206173206672616d655f73797374656d3a3a436f6e6669673e4f726967696e293aa4092d20546865207369676e6174757265206f66207468652063616c6c6572277320636f6c646b65792e00682a2027686f746b6579272028543a3a4163636f756e744964293afc092d2054686520686f746b6579207765206172652064656c65676174696e6720286d757374206265206f776e65642062792074686520636f6c646b65792e29003c2a202774616b65272028753634293a0101092d20546865207374616b652070726f706f7274696f6e2074686174207468697320686f746b65792074616b65732066726f6d2064656c65676174696f6e732e002023204576656e743a402a2044656c656761746541646465643bc8092d204f6e207375636365737366756c6c792073657474696e67206120686f746b657920617320612064656c65676174652e002423205261697365733a482a20274e6f7452656769737465726564273a0501092d2054686520686f746b6579207765206172652064656c65676174696e67206973206e6f742072656769737465726564206f6e20746865206e6574776f726b2e00642a20274e6f6e4173736f636961746564436f6c644b6579273a1101092d2054686520686f746b6579207765206172652064656c65676174696e67206973206e6f74206f776e6564206279207468652063616c6c696e6720636f6c646b65742e003464656372656173655f74616b65080118686f746b6579000130543a3a4163636f756e74496400011074616b659c010c753136004184c02d2d2d20416c6c6f77732064656c65676174657320746f206465637265617365206974732074616b652076616c75652e001c2320417267733ac82a20276f726967696e273a20283c54206173206672616d655f73797374656d3a3a436f6e6669673e3a3a4f726967696e293aa4092d20546865207369676e6174757265206f66207468652063616c6c6572277320636f6c646b65792e00682a2027686f746b6579272028543a3a4163636f756e744964293afc092d2054686520686f746b6579207765206172652064656c65676174696e6720286d757374206265206f776e65642062792074686520636f6c646b65792e2900442a20276e6574756964272028753136293a84092d205375626e657420494420746f2064656372656173652074616b6520666f72003c2a202774616b65272028753136293a1101092d20546865206e6577207374616b652070726f706f7274696f6e2074686174207468697320686f746b65792074616b65732066726f6d2064656c65676174696f6e732e1d0120202020202020546865206e65772076616c75652063616e206265206265747765656e203020616e642031315f37393620616e642073686f756c64206265207374726963746c793901202020202020206c6f776572207468616e207468652070726576696f75732076616c75652e204974205420697320746865206e65772076616c75652028726174696f6e616c206e756d626572292c3d01202020202020207468652074686520706172616d657465722069732063616c63756c61746564206173205b3635353335202a20545d2e20466f72206578616d706c652c20312520776f756c6420626598202020202020205b302e3031202a2036353533355d203d205b3635352e33355d203d20363535002023204576656e743a402a2054616b654465637265617365643bf0092d204f6e207375636365737366756c6c792073657474696e672061206465637265617365642074616b6520666f72207468697320686f746b65792e002423205261697365733a482a20274e6f7452656769737465726564273a0501092d2054686520686f746b6579207765206172652064656c65676174696e67206973206e6f742072656769737465726564206f6e20746865206e6574776f726b2e00642a20274e6f6e4173736f636961746564436f6c644b6579273a1101092d2054686520686f746b6579207765206172652064656c65676174696e67206973206e6f74206f776e6564206279207468652063616c6c696e6720636f6c646b65792e005c2a202744656c656761746554616b65546f6f4c6f77273a1d01092d205468652064656c65676174652069732073657474696e6720612074616b65207768696368206973206e6f74206c6f776572207468616e207468652070726576696f75732e0034696e6372656173655f74616b65080118686f746b6579000130543a3a4163636f756e74496400011074616b659c010c7531360042782d012d2d2d20416c6c6f77732064656c65676174657320746f20696e637265617365206974732074616b652076616c75652e20546869732063616c6c20697320726174652d6c696d697465642e001c2320417267733ac82a20276f726967696e273a20283c54206173206672616d655f73797374656d3a3a436f6e6669673e3a3a4f726967696e293aa4092d20546865207369676e6174757265206f66207468652063616c6c6572277320636f6c646b65792e00682a2027686f746b6579272028543a3a4163636f756e744964293afc092d2054686520686f746b6579207765206172652064656c65676174696e6720286d757374206265206f776e65642062792074686520636f6c646b65792e29003c2a202774616b65272028753136293a1101092d20546865206e6577207374616b652070726f706f7274696f6e2074686174207468697320686f746b65792074616b65732066726f6d2064656c65676174696f6e732e1d0120202020202020546865206e65772076616c75652063616e206265206265747765656e203020616e642031315f37393620616e642073686f756c64206265207374726963746c7935012020202020202067726561746572207468616e207468652070726576696f75732076616c75652e205420697320746865206e65772076616c75652028726174696f6e616c206e756d626572292c3d01202020202020207468652074686520706172616d657465722069732063616c63756c61746564206173205b3635353335202a20545d2e20466f72206578616d706c652c20312520776f756c6420626598202020202020205b302e3031202a2036353533355d203d205b3635352e33355d203d20363535002023204576656e743a402a2054616b65496e637265617365643bf0092d204f6e207375636365737366756c6c792073657474696e67206120696e637265617365642074616b6520666f72207468697320686f746b65792e002423205261697365733a482a20274e6f7452656769737465726564273a0501092d2054686520686f746b6579207765206172652064656c65676174696e67206973206e6f742072656769737465726564206f6e20746865206e6574776f726b2e00642a20274e6f6e4173736f636961746564436f6c644b6579273a1101092d2054686520686f746b6579207765206172652064656c65676174696e67206973206e6f74206f776e6564206279207468652063616c6c696e6720636f6c646b65792e00602a202744656c656761746554616b65546f6f48696768273a2501092d205468652064656c65676174652069732073657474696e6720612074616b65207768696368206973206e6f742067726561746572207468616e207468652070726576696f75732e00246164645f7374616b650c0118686f746b6579000130543a3a4163636f756e7449640001186e65747569649c010c753136000134616d6f756e745f7374616b656418010c753634000278d42d2d2d2041646473207374616b6520746f206120686f746b65792e205468652063616c6c206973206d6164652066726f6d2074686594636f6c646b6579206163636f756e74206c696e6b656420696e2074686520686f746b65792ee84f6e6c7920746865206173736f63696174656420636f6c646b657920697320616c6c6f77656420746f206d616b65207374616b696e6720616e64d0756e7374616b696e672072657175657374732e20546869732070726f746563747320746865206e6575726f6e20616761696e7374c461747461636b73206f6e2069747320686f746b65792072756e6e696e6720696e2070726f64756374696f6e20636f64652e001c2320417267733ac4202a20276f726967696e273a20283c54206173206672616d655f73797374656d3a3a436f6e6669673e4f726967696e293aa4092d20546865207369676e6174757265206f66207468652063616c6c6572277320636f6c646b65792e006c202a2027686f746b6579272028543a3a4163636f756e744964293a84092d20546865206173736f63696174656420686f746b6579206163636f756e742e0064202a2027616d6f756e745f7374616b6564272028753634293a0501092d2054686520616d6f756e74206f66207374616b6520746f20626520616464656420746f2074686520686f746b6579207374616b696e67206163636f756e742e002023204576656e743a38202a205374616b6541646465643be0092d204f6e20746865207375636365737366756c6c7920616464696e67207374616b6520746f206120676c6f62616c206163636f756e742e002423205261697365733a74202a20274e6f74456e6f75676842616c616e6365546f5374616b65273a1101092d204e6f7420656e6f7567682062616c616e6365206f6e2074686520636f6c646b657920746f20616464206f6e746f2074686520676c6f62616c206163636f756e742e0068202a20274e6f6e4173736f636961746564436f6c644b6579273ae8092d205468652063616c6c696e6720636f6c646b6579206973206e6f74206173736f6369617465642077697468207468697320686f746b65792e0070202a202742616c616e63655769746864726177616c4572726f72273ab020092d204572726f7273207374656d6d696e672066726f6d207472616e73616374696f6e2070616c6c65742e003072656d6f76655f7374616b650c0118686f746b6579000130543a3a4163636f756e7449640001186e65747569649c010c75313600013c616d6f756e745f756e7374616b656418010c753634000370f052656d6f7665207374616b652066726f6d20746865207374616b696e67206163636f756e742e205468652063616c6c206d757374206265206d6164651d0166726f6d2074686520636f6c646b6579206163636f756e7420617474616368656420746f20746865206e6575726f6e206d657461646174612e204f6e6c792074686973206b6579d8686173207065726d697373696f6e20746f206d616b65207374616b696e6720616e6420756e7374616b696e672072657175657374732e001c2320417267733ac02a20276f726967696e273a20283c54206173206672616d655f73797374656d3a3a436f6e6669673e4f726967696e293aa4092d20546865207369676e6174757265206f66207468652063616c6c6572277320636f6c646b65792e00682a2027686f746b6579272028543a3a4163636f756e744964293a84092d20546865206173736f63696174656420686f746b6579206163636f756e742e00682a2027616d6f756e745f756e7374616b6564272028753634293a0501092d2054686520616d6f756e74206f66207374616b6520746f20626520616464656420746f2074686520686f746b6579207374616b696e67206163636f756e742e002023204576656e743a3c2a205374616b6552656d6f7665643bf8092d204f6e20746865207375636365737366756c6c792072656d6f76696e67207374616b652066726f6d2074686520686f746b6579206163636f756e742e002423205261697365733a482a20274e6f7452656769737465726564273a2d01092d205468726f776e20696620746865206163636f756e742077652061726520617474656d7074696e6720746f20756e7374616b652066726f6d206973206e6f6e206578697374656e742e00642a20274e6f6e4173736f636961746564436f6c644b6579273a1d01092d205468726f776e2069662074686520636f6c646b657920646f6573206e6f74206f776e2074686520686f746b65792077652061726520756e7374616b696e672066726f6d2e00742a20274e6f74456e6f7567685374616b65546f5769746864726177273a3901092d205468726f776e206966207468657265206973206e6f7420656e6f756768207374616b65206f6e2074686520686f746b657920746f20776974686477726177207468697320616d6f756e742e002873657276655f61786f6e2001186e65747569649c010c75313600011c76657273696f6e10010c753332000108697020011075313238000110706f72749c010c75313600011c69705f74797065080108753800012070726f746f636f6c0801087538000130706c616365686f6c646572310801087538000130706c616365686f6c6465723208010875380004cca901536572766573206f7220757064617465732061786f6e202f70726f6d65746865757320696e666f726d6174696f6e20666f7220746865206e6575726f6e206173736f6369617465642077697468207468652063616c6c65722e204966207468652063616c6c6572206973ad01616c7265616479207265676973746572656420746865206d6574616461746120697320757064617465642e204966207468652063616c6c6572206973206e6f74207265676973746572656420746869732063616c6c207468726f7773204e6f74526567697374657265642e001c2320417267733ac02a20276f726967696e273a20283c54206173206672616d655f73797374656d3a3a436f6e6669673e4f726967696e293a7c092d20546865207369676e6174757265206f66207468652063616c6c65722e00442a20276e6574756964272028753136293a78092d2054686520753136206e6574776f726b206964656e7469666965722e00482a202776657273696f6e272028753634293a90092d205468652062697474656e736f722076657273696f6e206964656e7469666965722e00342a20276970272028753634293ae4092d2054686520656e64706f696e7420697020696e666f726d6174696f6e2061732061207531323820656e636f64656420696e74656765722e003c2a2027706f7274272028753136293ae8092d2054686520656e64706f696e7420706f727420696e666f726d6174696f6e20617320612075313620656e636f64656420696e74656765722e00442a202769705f747970652720287538293aac092d2054686520656e64706f696e742069702076657273696f6e20617320612075382c2034206f7220362e00482a202770726f746f636f6c2720287538293a44092d205544503a31206f72205443503a3000582a2027706c616365686f6c646572312720287538293aa0092d20506c616365686f6c64657220666f72206675727468657220657874726120706172616d732e00582a2027706c616365686f6c646572322720287538293aa0092d20506c616365686f6c64657220666f72206675727468657220657874726120706172616d732e002023204576656e743a342a2041786f6e5365727665643ba4092d204f6e207375636365737366756c6c792073657276696e67207468652061786f6e20696e666f2e002423205261697365733a6c2a20275375624e6574776f726b446f65734e6f744578697374273adc092d20417474656d7074696e6720746f207365742077656967687473206f6e2061206e6f6e2d6578697374656e74206e6574776f726b2e00482a20274e6f7452656769737465726564273aec092d20417474656d7074696e6720746f2073657420776569676874732066726f6d2061206e6f6e2072656769737465726564206163636f756e742e00482a2027496e76616c6964497054797065273a74092d205468652069702074797065206973206e6f742034206f7220362e00542a2027496e76616c6964497041646472657373273a1901092d20546865206e756d65726963616c6c7920656e636f646564206970206164647265737320646f6573206e6f74207265736f6c766520746f20612070726f7065722069702e00742a202753657276696e67526174654c696d69744578636565646564273a1d01092d20417474656d7074696e6720746f207365742070726f6d65746865757320696e666f726d6174696f6e2077697468696e67207468652072617465206c696d6974206d696e2e003873657276655f61786f6e5f746c732401186e65747569649c010c75313600011c76657273696f6e10010c753332000108697020011075313238000110706f72749c010c75313600011c69705f74797065080108753800012070726f746f636f6c0801087538000130706c616365686f6c646572310801087538000130706c616365686f6c64657232080108753800012c636572746966696361746538011c5665633c75383e0028dc2d0153616d65206173206073657276655f61786f6e60206275742074616b6573206120636572746966696361746520617320616e206578747261206f7074696f6e616c20617267756d656e742ea901536572766573206f7220757064617465732061786f6e202f70726f6d65746865757320696e666f726d6174696f6e20666f7220746865206e6575726f6e206173736f6369617465642077697468207468652063616c6c65722e204966207468652063616c6c6572206973ad01616c7265616479207265676973746572656420746865206d6574616461746120697320757064617465642e204966207468652063616c6c6572206973206e6f74207265676973746572656420746869732063616c6c207468726f7773204e6f74526567697374657265642e001c2320417267733ac02a20276f726967696e273a20283c54206173206672616d655f73797374656d3a3a436f6e6669673e4f726967696e293a7c092d20546865207369676e6174757265206f66207468652063616c6c65722e00442a20276e6574756964272028753136293a78092d2054686520753136206e6574776f726b206964656e7469666965722e00482a202776657273696f6e272028753634293a90092d205468652062697474656e736f722076657273696f6e206964656e7469666965722e00342a20276970272028753634293ae4092d2054686520656e64706f696e7420697020696e666f726d6174696f6e2061732061207531323820656e636f64656420696e74656765722e003c2a2027706f7274272028753136293ae8092d2054686520656e64706f696e7420706f727420696e666f726d6174696f6e20617320612075313620656e636f64656420696e74656765722e00442a202769705f747970652720287538293aac092d2054686520656e64706f696e742069702076657273696f6e20617320612075382c2034206f7220362e00482a202770726f746f636f6c2720287538293a44092d205544503a31206f72205443503a3000582a2027706c616365686f6c646572312720287538293aa0092d20506c616365686f6c64657220666f72206675727468657220657874726120706172616d732e00582a2027706c616365686f6c646572322720287538293aa0092d20506c616365686f6c64657220666f72206675727468657220657874726120706172616d732e00682a202763657274696669636174652720285665633c75383e293ad4202020202d20544c5320636572746966696361746520666f7220696e746572206e6575726f6e20636f6d6d756e69746174696f6e2e002023204576656e743a342a2041786f6e5365727665643ba4092d204f6e207375636365737366756c6c792073657276696e67207468652061786f6e20696e666f2e002423205261697365733a6c2a20275375624e6574776f726b446f65734e6f744578697374273adc092d20417474656d7074696e6720746f207365742077656967687473206f6e2061206e6f6e2d6578697374656e74206e6574776f726b2e00482a20274e6f7452656769737465726564273aec092d20417474656d7074696e6720746f2073657420776569676874732066726f6d2061206e6f6e2072656769737465726564206163636f756e742e00482a2027496e76616c6964497054797065273a74092d205468652069702074797065206973206e6f742034206f7220362e00542a2027496e76616c6964497041646472657373273a1901092d20546865206e756d65726963616c6c7920656e636f646564206970206164647265737320646f6573206e6f74207265736f6c766520746f20612070726f7065722069702e00742a202753657276696e67526174654c696d69744578636565646564273a1d01092d20417474656d7074696e6720746f207365742070726f6d65746865757320696e666f726d6174696f6e2077697468696e67207468652072617465206c696d6974206d696e2e004073657276655f70726f6d6574686575731401186e65747569649c010c75313600011c76657273696f6e10010c753332000108697020011075313238000110706f72749c010c75313600011c69705f747970650801087538000550bc2d2d2d2d205365742070726f6d65746865757320696e666f726d6174696f6e20666f7220746865206e6575726f6e2e1c2320417267733ac02a20276f726967696e273a20283c54206173206672616d655f73797374656d3a3a436f6e6669673e4f726967696e293a9c092d20546865207369676e6174757265206f66207468652063616c6c696e6720686f746b65792e00442a20276e6574756964272028753136293a78092d2054686520753136206e6574776f726b206964656e7469666965722e00482a202776657273696f6e272028753136293a94092d20205468652062697474656e736f722076657273696f6e206964656e7469666965722e00382a2027697027202875313238293aec092d205468652070726f6d65746865757320697020696e666f726d6174696f6e2061732061207531323820656e636f64656420696e74656765722e003c2a2027706f7274272028753136293af0092d205468652070726f6d65746865757320706f727420696e666f726d6174696f6e20617320612075313620656e636f64656420696e74656765722e00442a202769705f747970652720287538293a60092d205468652069702074797065207634206f722076362e002072656769737465721801186e65747569649c010c753136000130626c6f636b5f6e756d62657218010c7536340001146e6f6e636518010c753634000110776f726b38011c5665633c75383e000118686f746b6579000130543a3a4163636f756e74496400011c636f6c646b6579000130543a3a4163636f756e7449640006bcb82d2d2d2d205265676973746572732061206e6577206e6575726f6e20746f20746865207375626e6574776f726b2e001c2320417267733ac02a20276f726967696e273a20283c54206173206672616d655f73797374656d3a3a436f6e6669673e4f726967696e293a9c092d20546865207369676e6174757265206f66207468652063616c6c696e6720686f746b65792e00442a20276e6574756964272028753136293a78092d2054686520753136206e6574776f726b206964656e7469666965722e00642a2027626c6f636b5f6e756d6265722720282075363420293a98092d20426c6f636b2068617368207573656420746f2070726f766520776f726b20646f6e652e00482a20276e6f6e63652720282075363420293a98092d20506f73697469766520696e7465676572206e6f6e6365207573656420696e20504f572e00542a2027776f726b272028205665633c75383e20293abc092d20566563746f7220656e636f64656420627974657320726570726573656e74696e6720776f726b20646f6e652e00702a2027686f746b657927202820543a3a4163636f756e74496420293aa8092d20486f746b657920746f206265207265676973746572656420746f20746865206e6574776f726b2e00742a2027636f6c646b657927202820543a3a4163636f756e74496420293a78092d204173736f63696174656420636f6c646b6579206163636f756e742e002023204576656e743a4c2a204e6575726f6e526567697374657265643b1901092d204f6e207375636365737366756c6c79207265676973746572696e6720612075696420746f2061206e6575726f6e20736c6f74206f6e2061207375626e6574776f726b2e002423205261697365733a6c2a20275375624e6574776f726b446f65734e6f744578697374273ad0092d20417474656d7074696e6720746f20726567697374657220746f2061206e6f6e206578697374656e74206e6574776f726b2e00882a2027546f6f4d616e79526567697374726174696f6e7354686973426c6f636b273a2901092d205468697320726567697374726174696f6e20657863656564732074686520746f74616c20616c6c6f776564206f6e2074686973206e6574776f726b207468697320626c6f636b2e00902a2027486f744b6579416c726561647952656769737465726564496e5375624e6574273ad0092d2054686520686f746b657920697320616c72656164792072656769737465726564206f6e2074686973206e6574776f726b2e00542a2027496e76616c6964576f726b426c6f636b273a2501092d2054686520776f726b20686173206265656e20706572666f726d6564206f6e2061207374616c652c206675747572652c206f72206e6f6e206578697374656e7420626c6f636b2e00582a2027496e76616c6964446966666963756c7479273aa8092d2054686520776f726b20646f6573206e6f74206d617463682074686520646966666963756c74792e00402a2027496e76616c69645365616c273a64092d20546865207365616c20697320696e636f72726563742e0034726f6f745f7265676973746572040118686f746b6579000130543a3a4163636f756e744964003e048c52656769737465722074686520686f746b657920746f20726f6f74206e6574776f726b3461646a7573745f73656e617465040118686f746b6579000130543a3a4163636f756e744964003f04ec417474656d707420746f2061646a757374207468652073656e617465206d656d6265727368697020746f20696e636c756465206120686f746b65793c6275726e65645f72656769737465720801186e65747569649c010c753136000118686f746b6579000130543a3a4163636f756e744964000704c0557365722072656769737465722061206e6577207375626e6574776f726b20766961206275726e696e6720746f6b656e2c737761705f686f746b6579080118686f746b6579000130543a3a4163636f756e7449640001286e65775f686f746b6579000130543a3a4163636f756e744964004604ac5468652065787472696e73696320666f72207573657220746f206368616e67652069747320686f746b657930737761705f636f6c646b65790c012c6f6c645f636f6c646b6579000130543a3a4163636f756e74496400012c6e65775f636f6c646b6579000130543a3a4163636f756e744964000124737761705f636f737418010c75363400473c2d015468652065787472696e73696320666f72207573657220746f206368616e67652074686520636f6c646b6579206173736f6369617465642077697468207468656972206163636f756e742e002c2320417267756d656e7473001d012a20606f726967696e60202d20546865206f726967696e206f66207468652063616c6c2c206d757374206265207369676e656420627920746865206f6c6420636f6c646b65792e09012a20606f6c645f636f6c646b657960202d205468652063757272656e7420636f6c646b6579206173736f636961746564207769746820746865206163636f756e742e11012a20606e65775f636f6c646b657960202d20546865206e657720636f6c646b657920746f206265206173736f636961746564207769746820746865206163636f756e742e0024232052657475726e7300590152657475726e73206120604469737061746368526573756c7457697468506f7374496e666f6020696e6469636174696e672073756363657373206f72206661696c757265206f6620746865206f7065726174696f6e2e002023205765696768740019015765696768742069732063616c63756c61746564206261736564206f6e20746865206e756d626572206f6620646174616261736520726561647320616e64207772697465732e447365745f6368696c646b65795f74616b650c0118686f746b6579000130543a3a4163636f756e7449640001186e65747569649c010c75313600011074616b659c010c753136004b74a85365747320746865206368696c646b65792074616b6520666f72206120676976656e20686f746b65792e002d01546869732066756e6374696f6e20616c6c6f7773206120636f6c646b657920746f2073657420746865206368696c646b65792074616b6520666f72206120676976656e20686f746b65792e5501546865206368696c646b65792074616b652064657465726d696e6573207468652070726f706f7274696f6e206f66207374616b6520746861742074686520686f746b6579206b6565707320666f7220697473656c66a07768656e20646973747269627574696e67207374616b6520746f20697473206368696c6472656e2e00302320417267756d656e74733ae02a20606f726967696e6020283c54206173206672616d655f73797374656d3a3a436f6e6669673e3a3a52756e74696d654f726967696e293a8901202020202d20546865207369676e6174757265206f66207468652063616c6c696e6720636f6c646b65792e2053657474696e67206368696c646b65792074616b652063616e206f6e6c7920626520646f6e652062792074686520636f6c646b65792e00682a2060686f746b6579602028543a3a4163636f756e744964293ae4202020202d2054686520686f746b657920666f7220776869636820746865206368696c646b65792074616b652077696c6c206265207365742e003c2a206074616b65602028753136293a8d01202020202d20546865206e6577206368696c646b65792074616b652076616c75652e205468697320697320612070657263656e7461676520726570726573656e74656420617320612076616c7565206265747765656e203020616e642031303030302c88202020202020776865726520313030303020726570726573656e747320313030252e002423204576656e74733a502a20604368696c646b657954616b65536574603af4202020202d204f6e207375636365737366756c6c792073657474696e6720746865206368696c646b65792074616b6520666f72206120686f746b65792e002423204572726f72733a642a20604e6f6e4173736f636961746564436f6c644b6579603aa8202020202d2054686520636f6c646b657920646f6573206e6f74206f776e2074686520686f746b65792e602a2060496e76616c69644368696c646b657954616b65603a4501202020202d205468652070726f76696465642074616b652076616c756520697320696e76616c6964202867726561746572207468616e20746865206d6178696d756d20616c6c6f7765642074616b65292e902a206054784368696c646b657954616b65526174654c696d69744578636565646564603a0901202020202d205468652072617465206c696d697420666f72206368616e67696e67206368696c646b65792074616b6520686173206265656e2065786365656465642e00907375646f5f7365745f74785f6368696c646b65795f74616b655f726174655f6c696d697404013474785f726174655f6c696d697418010c75363400452cec5365747320746865207472616e73616374696f6e2072617465206c696d697420666f72206368616e67696e67206368696c646b65792074616b652e00d0546869732066756e6374696f6e2063616e206f6e6c792062652063616c6c65642062792074686520726f6f74206f726967696e2e00302320417267756d656e74733ac82a20606f726967696e60202d20546865206f726967696e206f66207468652063616c6c2c206d75737420626520726f6f742ec42a206074785f726174655f6c696d697460202d20546865206e65772072617465206c696d697420696e20626c6f636b732e002423204572726f72733aa82a20604261644f726967696e60202d20496620746865206f726967696e206973206e6f7420726f6f742e00687375646f5f7365745f6d696e5f6368696c646b65795f74616b6504011074616b659c010c753136004c2c9c5365747320746865206d696e696d756d20616c6c6f776564206368696c646b65792074616b652e00d0546869732066756e6374696f6e2063616e206f6e6c792062652063616c6c65642062792074686520726f6f74206f726967696e2e00302320417267756d656e74733ac82a20606f726967696e60202d20546865206f726967696e206f66207468652063616c6c2c206d75737420626520726f6f742ebc2a206074616b6560202d20546865206e6577206d696e696d756d206368696c646b65792074616b652076616c75652e002423204572726f72733aa82a20604261644f726967696e60202d20496620746865206f726967696e206973206e6f7420726f6f742e00687375646f5f7365745f6d61785f6368696c646b65795f74616b6504011074616b659c010c753136004d2c9c5365747320746865206d6178696d756d20616c6c6f776564206368696c646b65792074616b652e00d0546869732066756e6374696f6e2063616e206f6e6c792062652063616c6c65642062792074686520726f6f74206f726967696e2e00302320417267756d656e74733ac82a20606f726967696e60202d20546865206f726967696e206f66207468652063616c6c2c206d75737420626520726f6f742ebc2a206074616b6560202d20546865206e6577206d6178696d756d206368696c646b65792074616b652076616c75652e002423204572726f72733aa82a20604261644f726967696e60202d20496620746865206f726967696e206973206e6f7420726f6f742e00107375646f04011063616c6c1503015c426f783c543a3a5375646f52756e74696d6543616c6c3e0033184d0141757468656e74696361746573206120636f756e63696c2070726f706f73616c20616e64206469737061746368657320612066756e6374696f6e2063616c6c20776974682060526f6f7460206f726967696e2e00f4546865206469737061746368206f726967696e20666f7220746869732063616c6c206d757374206265206120636f756e63696c206d616a6f726974792e0034232320436f6d706c65786974791c2d204f2831292e547375646f5f756e636865636b65645f77656967687408011063616c6c1503015c426f783c543a3a5375646f52756e74696d6543616c6c3e0001187765696768742c01185765696768740034204d0141757468656e74696361746573206120636f756e63696c2070726f706f73616c20616e64206469737061746368657320612066756e6374696f6e2063616c6c20776974682060526f6f7460206f726967696e2e2d01546869732066756e6374696f6e20646f6573206e6f7420636865636b2074686520776569676874206f66207468652063616c6c2c20616e6420696e737465616420616c6c6f7773207468659c7573657220746f20737065636966792074686520776569676874206f66207468652063616c6c2e00f4546865206469737061746368206f726967696e20666f7220746869732063616c6c206d757374206265206120636f756e63696c206d616a6f726974792e0034232320436f6d706c65786974791c2d204f2831292e10766f7465100118686f746b6579000130543a3a4163636f756e74496400012070726f706f73616c34011c543a3a48617368000114696e6465786101010c75333200011c617070726f7665240110626f6f6c0037045c5573657220766f7465206f6e20612070726f706f73616c4072656769737465725f6e6574776f726b040118686f746b6579000130543a3a4163636f756e744964003b0478557365722072656769737465722061206e6577207375626e6574776f726b186661756365740c0130626c6f636b5f6e756d62657218010c7536340001146e6f6e636518010c753634000110776f726b38011c5665633c75383e003c0cd0466163696c6974792065787472696e73696320666f72207573657220746f206765742074616b656e2066726f6d20666175636574d04974206973206f6e6c7920617661696c61626c65207768656e20706f772d666175636574206665617475726520656e61626c6564dc4a757374206465706c6f79656420696e20746573746e657420616e64206465766e657420666f722074657374696e6720707572706f736540646973736f6c76655f6e6574776f726b08011c636f6c646b6579000130543a3a4163636f756e7449640001186e65747569649c010c753136003d086852656d6f7665206120757365722773207375626e6574776f726bac5468652063616c6c6572206d75737420626520746865206f776e6572206f6620746865206e6574776f726b307365745f6368696c6472656e0c0118686f746b6579000130543a3a4163636f756e7449640001186e65747569649c010c7531360001206368696c6472656eac01605665633c287536342c20543a3a4163636f756e744964293e0043b0f453657420612073696e676c65206368696c6420666f72206120676976656e20686f746b6579206f6e206120737065636966696564206e6574776f726b2e007d01546869732066756e6374696f6e20616c6c6f7773206120636f6c646b657920746f2073657420612073696e676c65206368696c6420666f72206120676976656e20686f746b6579206f6e206120737065636966696564206e6574776f726b2e51015468652070726f706f7274696f6e206f662074686520686f746b65792773207374616b6520746f20626520616c6c6f636174656420746f20746865206368696c6420697320616c736f207370656369666965642e00302320417267756d656e74733ae02a20606f726967696e6020283c54206173206672616d655f73797374656d3a3a436f6e6669673e3a3a52756e74696d654f726967696e293a8d01202020202d20546865207369676e6174757265206f66207468652063616c6c696e6720636f6c646b65792e2053657474696e67206120686f746b6579206368696c642063616e206f6e6c7920626520646f6e652062792074686520636f6c646b65792e00682a2060686f746b6579602028543a3a4163636f756e744964293ac8202020202d2054686520686f746b65792077686963682077696c6c2062652061737369676e656420746865206368696c642e00642a20606368696c64602028543a3a4163636f756e744964293ad4202020202d20546865206368696c642077686963682077696c6c2062652061737369676e656420746f2074686520686f746b65792e00442a20606e6574756964602028753136293afc202020202d2054686520753136206e6574776f726b206964656e74696669657220776865726520746865206368696c646b65792077696c6c2065786973742e00542a206070726f706f7274696f6e602028753634293a8901202020202d2050726f706f7274696f6e206f662074686520686f746b65792773207374616b6520746f20626520676976656e20746f20746865206368696c642c207468652076616c7565206d75737420626520753634206e6f726d616c697a65642e002423204576656e74733a5c2a20604368696c64416464656453696e67756c6172603ad8202020202d204f6e207375636365737366756c6c79207265676973746572696e672061206368696c6420746f206120686f746b65792e002423204572726f72733a6c2a20605375624e6574776f726b446f65734e6f744578697374603adc202020202d20417474656d7074696e6720746f20726567697374657220746f2061206e6f6e2d6578697374656e74206e6574776f726b2ea42a2060526567697374726174696f6e4e6f745065726d69747465644f6e526f6f745375626e6574603ae4202020202d20417474656d7074696e6720746f2072656769737465722061206368696c64206f6e2074686520726f6f74206e6574776f726b2e642a20604e6f6e4173736f636961746564436f6c644b6579603a4501202020202d2054686520636f6c646b657920646f6573206e6f74206f776e2074686520686f746b6579206f7220746865206368696c64206973207468652073616d652061732074686520686f746b65792e6c2a2060486f744b65794163636f756e744e6f74457869737473603aa0202020202d2054686520686f746b6579206163636f756e7420646f6573206e6f742065786973742e0084232044657461696c6564204578706c616e6174696f6e206f6620436865636b733aa501312e202a2a5369676e617475726520566572696669636174696f6e2a2a3a20456e73757265732074686174207468652063616c6c657220686173207369676e656420746865207472616e73616374696f6e2c20766572696679696e672074686520636f6c646b65792ef901322e202a2a526f6f74204e6574776f726b20436865636b2a2a3a20456e73757265732074686174207468652064656c65676174696f6e206973206e6f74206f6e2074686520726f6f74206e6574776f726b2c206173206368696c6420686f746b65797320617265206e6f742076616c6964206f6e2074686520726f6f742e2901332e202a2a4e6574776f726b204578697374656e636520436865636b2a2a3a20456e737572657320746861742074686520737065636966696564206e6574776f726b206578697374732e2101342e202a2a4f776e65727368697020566572696669636174696f6e2a2a3a20456e737572657320746861742074686520636f6c646b6579206f776e732074686520686f746b65792e5901352e202a2a486f746b6579204163636f756e74204578697374656e636520436865636b2a2a3a20456e737572657320746861742074686520686f746b6579206163636f756e7420616c7265616479206578697374732e5901362e202a2a4368696c642d486f746b65792044697374696e6374696f6e2a2a3a20456e7375726573207468617420746865206368696c64206973206e6f74207468652073616d652061732074686520686f746b65792e6501372e202a2a4f6c64204368696c6472656e20436c65616e75702a2a3a2052656d6f7665732074686520686f746b65792066726f6d2074686520706172656e74206c697374206f6620697473206f6c64206368696c6472656e2ec901382e202a2a4e6577204368696c6472656e2041737369676e6d656e742a2a3a2041737369676e7320746865206e6577206368696c6420746f2074686520686f746b657920616e6420757064617465732074686520706172656e74206c69737420666f7220746865206e6577206368696c642e547363686564756c655f737761705f636f6c646b657904012c6e65775f636f6c646b6579000130543a3a4163636f756e74496400498011015363686564756c6573206120636f6c646b65792073776170206f7065726174696f6e20746f20626520657865637574656420617420612066757475726520626c6f636b2e004901546869732066756e6374696f6e20616c6c6f77732061207573657220746f207363686564756c6520746865207377617070696e67206f6620746865697220636f6c646b657920746f2061206e6577206f6e65490161742061207370656369666965642066757475726520626c6f636b2e205468652073776170206973206e6f7420657865637574656420696d6d6564696174656c7920627574206973207363686564756c65649c746f206f63637572206174207468652073706563696669656420626c6f636b206e756d6265722e002c2320417267756d656e74730065012a20606f726967696e60202d20546865206f726967696e206f66207468652063616c6c2c2077686963682073686f756c64206265207369676e6564206279207468652063757272656e7420636f6c646b6579206f776e65722e59012a20606e65775f636f6c646b657960202d20546865206163636f756e74204944206f6620746865206e657720636f6c646b657920746861742077696c6c207265706c616365207468652063757272656e74206f6e652e25012a20607768656e60202d2054686520626c6f636b206e756d6265722061742077686963682074686520636f6c646b657920737761702073686f756c642062652065786563757465642e0024232052657475726e7300610152657475726e73206120604469737061746368526573756c7457697468506f7374496e666f6020696e6469636174696e67207768657468657220746865207363686564756c696e6720776173207375636365737366756c2e002023204572726f72730094546869732066756e6374696f6e206d61792072657475726e20616e206572726f722069663a6c2a20546865206f726967696e206973206e6f74207369676e65642ef82a20546865207363686564756c696e67206661696c732064756520746f20636f6e666c69637473206f722073797374656d20636f6e73747261696e74732e001c23204e6f7465730071012d205468652061637475616c2073776170206973206e6f7420706572666f726d656420627920746869732066756e6374696f6e2e204974206d6572656c79207363686564756c6573207468652073776170206f7065726174696f6e2e81012d2054686520776569676874206f6620746869732063616c6c2069732073657420746f20612066697865642076616c756520616e64206d6179206e6565642061646a7573746d656e74206261736564206f6e2062656e63686d61726b696e672e00182320544f444f003d012d20496d706c656d656e742070726f706572207765696768742063616c63756c6174696f6e206261736564206f6e2074686520636f6d706c6578697479206f6620746865206f7065726174696f6e2e1d012d20436f6e736964657220616464696e6720636865636b7320746f2070726576656e74207363686564756c696e6720746f6f2066617220696e746f20746865206675747572652e64544f444f3a2042656e63686d61726b20746869732063616c6c647363686564756c655f646973736f6c76655f6e6574776f726b0401186e65747569649c010c753136004a3809015363686564756c652074686520646973736f6c7574696f6e206f662061206e6574776f726b20617420612073706563696669656420626c6f636b206e756d6265722e002c2320417267756d656e74730009012a20606f726967696e60202d20546865206f726967696e206f66207468652063616c6c2c206d757374206265207369676e6564206279207468652073656e6465722ee02a20606e657475696460202d2054686520753136206e6574776f726b206964656e74696669657220746f20626520646973736f6c7665642e0024232052657475726e7300590152657475726e73206120604469737061746368526573756c7457697468506f7374496e666f6020696e6469636174696e672073756363657373206f72206661696c757265206f6620746865206f7065726174696f6e2e002023205765696768740019015765696768742069732063616c63756c61746564206261736564206f6e20746865206e756d626572206f6620646174616261736520726561647320616e64207772697465732e307365745f6964656e746974791801106e616d6538011c5665633c75383e00010c75726c38011c5665633c75383e000114696d61676538011c5665633c75383e00011c646973636f726438011c5665633c75383e00012c6465736372697074696f6e38011c5665633c75383e0001286164646974696f6e616c38011c5665633c75383e004450bc2d2d2d2d205365742070726f6d65746865757320696e666f726d6174696f6e20666f7220746865206e6575726f6e2e1c2320417267733ac02a20276f726967696e273a20283c54206173206672616d655f73797374656d3a3a436f6e6669673e4f726967696e293a9c092d20546865207369676e6174757265206f66207468652063616c6c696e6720686f746b65792e00442a20276e6574756964272028753136293a78092d2054686520753136206e6574776f726b206964656e7469666965722e00482a202776657273696f6e272028753136293a94092d20205468652062697474656e736f722076657273696f6e206964656e7469666965722e00382a2027697027202875313238293aec092d205468652070726f6d65746865757320697020696e666f726d6174696f6e2061732061207531323820656e636f64656420696e74656765722e003c2a2027706f7274272028753136293af0092d205468652070726f6d65746865757320706f727420696e666f726d6174696f6e20617320612075313620656e636f64656420696e74656765722e00442a202769705f747970652720287538293a60092d205468652069702074797065207634206f722076362e004c7365745f7375626e65745f6964656e746974791001186e65747569649c010c75313600012c7375626e65745f6e616d6538011c5665633c75383e00012c6769746875625f7265706f38011c5665633c75383e0001387375626e65745f636f6e7461637438011c5665633c75383e004e40bc2d2d2d2d2053657420746865206964656e7469747920696e666f726d6174696f6e20666f722061207375626e65742e1c2320417267733acc2a20606f726967696e60202d20283c54206173206672616d655f73797374656d3a3a436f6e6669673e3a3a4f726967696e293a4901202020202d20546865207369676e6174757265206f66207468652063616c6c696e6720636f6c646b65792c207768696368206d75737420626520746865206f776e6572206f6620746865207375626e65742e00442a20606e6574756964602028753136293ac8202020202d2054686520756e69717565206e6574776f726b206964656e746966696572206f6620746865207375626e65742e00682a20607375626e65745f6e616d656020285665633c75383e293a74202020202d20546865206e616d65206f6620746865207375626e65742e00682a20606769746875625f7265706f6020285665633c75383e293a0101202020202d2054686520476974487562207265706f7369746f7279206173736f636961746564207769746820746865207375626e6574206964656e746974792e00742a20607375626e65745f636f6e746163746020285665633c75383e293ab4202020202d2054686520636f6e7461637420696e666f726d6174696f6e20666f7220746865207375626e65742e7872656769737465725f6e6574776f726b5f776974685f6964656e74697479080118686f746b6579000130543a3a4163636f756e7449640001206964656e74697479fd0501604f7074696f6e3c5375626e65744964656e746974794f663e004f0478557365722072656769737465722061206e6577207375626e6574776f726b2c756e7374616b655f616c6c040118686f746b6579000130543a3a4163636f756e74496400536435022d2d2d2d2054686520696d706c656d656e746174696f6e20666f72207468652065787472696e73696320756e7374616b655f616c6c3a2052656d6f76657320616c6c207374616b652066726f6d206120686f746b6579206163636f756e74206163726f737320616c6c207375626e65747320616e642061646473206974206f6e746f206120636f6c646b65792e001c2320417267733acc2a20606f726967696e60202d20283c54206173206672616d655f73797374656d3a3a436f6e6669673e3a3a4f726967696e293ab0202020202d20546865207369676e6174757265206f66207468652063616c6c6572277320636f6c646b65792e00682a2060686f746b6579602028543a3a4163636f756e744964293a90202020202d20546865206173736f63696174656420686f746b6579206163636f756e742e002023204576656e743a3c2a205374616b6552656d6f7665643b0501202020202d204f6e20746865207375636365737366756c6c792072656d6f76696e67207374616b652066726f6d2074686520686f746b6579206163636f756e742e002423205261697365733a482a20604e6f7452656769737465726564603a3901202020202d205468726f776e20696620746865206163636f756e742077652061726520617474656d7074696e6720746f20756e7374616b652066726f6d206973206e6f6e206578697374656e742e00642a20604e6f6e4173736f636961746564436f6c644b6579603a2901202020202d205468726f776e2069662074686520636f6c646b657920646f6573206e6f74206f776e2074686520686f746b65792077652061726520756e7374616b696e672066726f6d2e00742a20604e6f74456e6f7567685374616b65546f5769746864726177603a4101202020202d205468726f776e206966207468657265206973206e6f7420656e6f756768207374616b65206f6e2074686520686f746b657920746f207769746864726177207468697320616d6f756e742e00602a20605478526174654c696d69744578636565646564603ac8202020202d205468726f776e206966206b65792068617320686974207472616e73616374696f6e2072617465206c696d697444756e7374616b655f616c6c5f616c706861040118686f746b6579000130543a3a4163636f756e74496400546435022d2d2d2d2054686520696d706c656d656e746174696f6e20666f72207468652065787472696e73696320756e7374616b655f616c6c3a2052656d6f76657320616c6c207374616b652066726f6d206120686f746b6579206163636f756e74206163726f737320616c6c207375626e65747320616e642061646473206974206f6e746f206120636f6c646b65792e001c2320417267733acc2a20606f726967696e60202d20283c54206173206672616d655f73797374656d3a3a436f6e6669673e3a3a4f726967696e293ab0202020202d20546865207369676e6174757265206f66207468652063616c6c6572277320636f6c646b65792e00682a2060686f746b6579602028543a3a4163636f756e744964293a90202020202d20546865206173736f63696174656420686f746b6579206163636f756e742e002023204576656e743a3c2a205374616b6552656d6f7665643b0501202020202d204f6e20746865207375636365737366756c6c792072656d6f76696e67207374616b652066726f6d2074686520686f746b6579206163636f756e742e002423205261697365733a482a20604e6f7452656769737465726564603a3901202020202d205468726f776e20696620746865206163636f756e742077652061726520617474656d7074696e6720746f20756e7374616b652066726f6d206973206e6f6e206578697374656e742e00642a20604e6f6e4173736f636961746564436f6c644b6579603a2901202020202d205468726f776e2069662074686520636f6c646b657920646f6573206e6f74206f776e2074686520686f746b65792077652061726520756e7374616b696e672066726f6d2e00742a20604e6f74456e6f7567685374616b65546f5769746864726177603a4101202020202d205468726f776e206966207468657265206973206e6f7420656e6f756768207374616b65206f6e2074686520686f746b657920746f207769746864726177207468697320616d6f756e742e00602a20605478526174654c696d69744578636565646564603ac8202020202d205468726f776e206966206b65792068617320686974207472616e73616374696f6e2072617465206c696d6974286d6f76655f7374616b651401346f726967696e5f686f746b6579000130543a3a4163636f756e74496400014864657374696e6174696f6e5f686f746b6579000130543a3a4163636f756e7449640001346f726967696e5f6e65747569649c010c75313600014864657374696e6174696f6e5f6e65747569649c010c753136000130616c7068615f616d6f756e7418010c753634005554f9012d2d2d2d2054686520696d706c656d656e746174696f6e20666f72207468652065787472696e736963206d6f76655f7374616b653a204d6f7665732073706563696669656420616d6f756e74206f66207374616b652066726f6d206120686f746b657920746f20616e6f74686572206163726f7373207375626e6574732e001c2320417267733acc2a20606f726967696e60202d20283c54206173206672616d655f73797374656d3a3a436f6e6669673e3a3a4f726967696e293ab0202020202d20546865207369676e6174757265206f66207468652063616c6c6572277320636f6c646b65792e00842a20606f726967696e5f686f746b6579602028543a3a4163636f756e744964293ab0202020202d2054686520686f746b6579206163636f756e7420746f206d6f7665207374616b652066726f6d2e00982a206064657374696e6174696f6e5f686f746b6579602028543a3a4163636f756e744964293aa8202020202d2054686520686f746b6579206163636f756e7420746f206d6f7665207374616b6520746f2e00842a20606f726967696e5f6e6574756964602028543a3a4163636f756e744964293a9c202020202d20546865207375626e657420494420746f206d6f7665207374616b652066726f6d2e00982a206064657374696e6174696f6e5f6e6574756964602028543a3a4163636f756e744964293a94202020202d20546865207375626e657420494420746f206d6f7665207374616b6520746f2e00802a2060616c7068615f616d6f756e74602028543a3a4163636f756e744964293a94202020202d2054686520616c706861207374616b6520616d6f756e7420746f206d6f76652e00387472616e736665725f7374616b6514014c64657374696e6174696f6e5f636f6c646b6579000130543a3a4163636f756e744964000118686f746b6579000130543a3a4163636f756e7449640001346f726967696e5f6e65747569649c010c75313600014864657374696e6174696f6e5f6e65747569649c010c753136000130616c7068615f616d6f756e7418010c75363400565475015472616e736665727320612073706563696669656420616d6f756e74206f66207374616b652066726f6d206f6e6520636f6c646b657920746f20616e6f746865722c206f7074696f6e616c6c79206163726f7373207375626e6574732c787768696c65206b656570696e67207468652073616d6520686f746b65792e002c2320417267756d656e747365012a20606f726967696e60202d20546865206f726967696e206f6620746865207472616e73616374696f6e2c207768696368206d757374206265207369676e65642062792074686520606f726967696e5f636f6c646b6579602e21012a206064657374696e6174696f6e5f636f6c646b657960202d2054686520636f6c646b657920746f20776869636820746865207374616b65206973207472616e736665727265642ec82a2060686f746b657960202d2054686520686f746b6579206173736f636961746564207769746820746865207374616b652ef42a20606f726967696e5f6e657475696460202d20546865206e6574776f726b2f7375626e657420494420746f206d6f7665207374616b652066726f6d2e71012a206064657374696e6174696f6e5f6e657475696460202d20546865206e6574776f726b2f7375626e657420494420746f206d6f7665207374616b6520746f2028666f722063726f73732d7375626e6574207472616e73666572292ecc2a2060616c7068615f616d6f756e7460202d2054686520616d6f756e74206f66207374616b6520746f207472616e736665722e002023204572726f72735052657475726e7320616e206572726f722069663ac82a20546865206f726967696e206973206e6f74207369676e65642062792074686520636f727265637420636f6c646b65792e7c2a20456974686572207375626e657420646f6573206e6f742065786973742e702a2054686520686f746b657920646f6573206e6f742065786973742e2d012a20546865726520697320696e73756666696369656e74207374616b65206f6e2060286f726967696e5f636f6c646b65792c20686f746b65792c206f726967696e5f6e657475696429602ef42a20546865207472616e7366657220616d6f756e742069732062656c6f7720746865206d696e696d756d207374616b6520726571756972656d656e742e002023204576656e7473bc4d617920656d6974206120605374616b655472616e7366657272656460206576656e74206f6e20737563636573732e28737761705f7374616b65100118686f746b6579000130543a3a4163636f756e7449640001346f726967696e5f6e65747569649c010c75313600014864657374696e6174696f6e5f6e65747569649c010c753136000130616c7068615f616d6f756e7418010c75363400574ca101537761707320612073706563696669656420616d6f756e74206f66207374616b652066726f6d206f6e65207375626e657420746f20616e6f746865722c207768696c65206b656570696e67207468652073616d6520636f6c646b657920616e6420686f746b65792e002c2320417267756d656e74739d012a20606f726967696e60202d20546865206f726967696e206f6620746865207472616e73616374696f6e2c207768696368206d757374206265207369676e65642062792074686520636f6c646b65792074686174206f776e73207468652060686f746b6579602ed42a2060686f746b657960202d2054686520686f746b65792077686f7365207374616b65206973206265696e6720737761707065642e19012a20606f726967696e5f6e657475696460202d20546865206e6574776f726b2f7375626e65742049442066726f6d207768696368207374616b652069732072656d6f7665642e1d012a206064657374696e6174696f6e5f6e657475696460202d20546865206e6574776f726b2f7375626e657420494420746f207768696368207374616b652069732061646465642ebc2a2060616c7068615f616d6f756e7460202d2054686520616d6f756e74206f66207374616b6520746f20737761702e002023204572726f72735052657475726e7320616e206572726f722069663a6d012a20546865207472616e73616374696f6e206973206e6f74207369676e65642062792074686520636f727265637420636f6c646b65792028692e652e2c2060636f6c646b65795f6f776e735f686f746b657960206661696c73292e01012a2045697468657220606f726967696e5f6e657475696460206f72206064657374696e6174696f6e5f6e65747569646020646f6573206e6f742065786973742e702a2054686520686f746b657920646f6573206e6f742065786973742e11012a20546865726520697320696e73756666696369656e74207374616b65206f6e206028636f6c646b65792c20686f746b65792c206f726967696e5f6e657475696429602ee42a20546865207377617020616d6f756e742069732062656c6f7720746865206d696e696d756d207374616b6520726571756972656d656e742e002023204576656e7473ac4d617920656d6974206120605374616b655377617070656460206576656e74206f6e20737563636573732e0c6101446973706174636861626c652066756e6374696f6e7320616c6c6f7720757365727320746f20696e7465726163742077697468207468652070616c6c657420616e6420696e766f6b65207374617465206368616e6765732e590154686573652066756e6374696f6e73206d6174657269616c697a65206173202265787472696e73696373222c20776869636820617265206f6674656e20636f6d706172656420746f207472616e73616374696f6e732e6101446973706174636861626c652066756e6374696f6e73206d75737420626520616e6e6f7461746564207769746820612077656967687420616e64206d7573742072657475726e2061204469737061746368526573756c742e01030000020503000503000002090300090300000408bcbc000d0300000230001103000002b50200150308586e6f64655f73756274656e736f725f72756e74696d652c52756e74696d6543616c6c00015c1853797374656d0400690101ad0173656c663a3a73705f6170695f68696464656e5f696e636c756465735f636f6e7374727563745f72756e74696d653a3a68696464656e5f696e636c7564653a3a64697370617463680a3a3a43616c6c61626c6543616c6c466f723c53797374656d2c2052756e74696d653e0000002454696d657374616d700400b10101b90173656c663a3a73705f6170695f68696464656e5f696e636c756465735f636f6e7374727563745f72756e74696d653a3a68696464656e5f696e636c7564653a3a64697370617463680a3a3a43616c6c61626c6543616c6c466f723c54696d657374616d702c2052756e74696d653e0002001c4772616e6470610400d50101b10173656c663a3a73705f6170695f68696464656e5f696e636c756465735f636f6e7374727563745f72756e74696d653a3a68696464656e5f696e636c7564653a3a64697370617463680a3a3a43616c6c61626c6543616c6c466f723c4772616e6470612c2052756e74696d653e0004002042616c616e6365730400510201b50173656c663a3a73705f6170695f68696464656e5f696e636c756465735f636f6e7374727563745f72756e74696d653a3a68696464656e5f696e636c7564653a3a64697370617463680a3a3a43616c6c61626c6543616c6c466f723c42616c616e6365732c2052756e74696d653e0005003c53756274656e736f724d6f64756c650400fd0201d10173656c663a3a73705f6170695f68696464656e5f696e636c756465735f636f6e7374727563745f72756e74696d653a3a68696464656e5f696e636c7564653a3a64697370617463680a3a3a43616c6c61626c6543616c6c466f723c53756274656e736f724d6f64756c652c2052756e74696d653e0007002c547269756d7669726174650400190301c10173656c663a3a73705f6170695f68696464656e5f696e636c756465735f636f6e7374727563745f72756e74696d653a3a68696464656e5f696e636c7564653a3a64697370617463680a3a3a43616c6c61626c6543616c6c466f723c547269756d7669726174652c2052756e74696d653e00080048547269756d7669726174654d656d6265727304001d0301dd0173656c663a3a73705f6170695f68696464656e5f696e636c756465735f636f6e7374727563745f72756e74696d653a3a68696464656e5f696e636c7564653a3a64697370617463680a3a3a43616c6c61626c6543616c6c466f723c547269756d7669726174654d656d626572732c2052756e74696d653e0009003453656e6174654d656d626572730400210301c90173656c663a3a73705f6170695f68696464656e5f696e636c756465735f636f6e7374727563745f72756e74696d653a3a68696464656e5f696e636c7564653a3a64697370617463680a3a3a43616c6c61626c6543616c6c466f723c53656e6174654d656d626572732c2052756e74696d653e000a001c5574696c6974790400250301b10173656c663a3a73705f6170695f68696464656e5f696e636c756465735f636f6e7374727563745f72756e74696d653a3a68696464656e5f696e636c7564653a3a64697370617463680a3a3a43616c6c61626c6543616c6c466f723c5574696c6974792c2052756e74696d653e000b00105375646f04003d0301a50173656c663a3a73705f6170695f68696464656e5f696e636c756465735f636f6e7374727563745f72756e74696d653a3a68696464656e5f696e636c7564653a3a64697370617463680a3a3a43616c6c61626c6543616c6c466f723c5375646f2c2052756e74696d653e000c00204d756c74697369670400410301b50173656c663a3a73705f6170695f68696464656e5f696e636c756465735f636f6e7374727563745f72756e74696d653a3a68696464656e5f696e636c7564653a3a64697370617463680a3a3a43616c6c61626c6543616c6c466f723c4d756c74697369672c2052756e74696d653e000d0020507265696d6167650400490301b50173656c663a3a73705f6170695f68696464656e5f696e636c756465735f636f6e7374727563745f72756e74696d653a3a68696464656e5f696e636c7564653a3a64697370617463680a3a3a43616c6c61626c6543616c6c466f723c507265696d6167652c2052756e74696d653e000e00245363686564756c657204004d0301b90173656c663a3a73705f6170695f68696464656e5f696e636c756465735f636f6e7374727563745f72756e74696d653a3a68696464656e5f696e636c7564653a3a64697370617463680a3a3a43616c6c61626c6543616c6c466f723c5363686564756c65722c2052756e74696d653e000f001450726f78790400550301a90173656c663a3a73705f6170695f68696464656e5f696e636c756465735f636f6e7374727563745f72756e74696d653a3a68696464656e5f696e636c7564653a3a64697370617463680a3a3a43616c6c61626c6543616c6c466f723c50726f78792c2052756e74696d653e00100020526567697374727904005d0301b50173656c663a3a73705f6170695f68696464656e5f696e636c756465735f636f6e7374727563745f72756e74696d653a3a68696464656e5f696e636c7564653a3a64697370617463680a3a3a43616c6c61626c6543616c6c466f723c52656769737472792c2052756e74696d653e0011002c436f6d6d69746d656e74730400690401c10173656c663a3a73705f6170695f68696464656e5f696e636c756465735f636f6e7374727563745f72756e74696d653a3a68696464656e5f696e636c7564653a3a64697370617463680a3a3a43616c6c61626c6543616c6c466f723c436f6d6d69746d656e74732c2052756e74696d653e0012002841646d696e5574696c7304007d0501bd0173656c663a3a73705f6170695f68696464656e5f696e636c756465735f636f6e7374727563745f72756e74696d653a3a68696464656e5f696e636c7564653a3a64697370617463680a3a3a43616c6c61626c6543616c6c466f723c41646d696e5574696c732c2052756e74696d653e00130020536166654d6f64650400810501b50173656c663a3a73705f6170695f68696464656e5f696e636c756465735f636f6e7374727563745f72756e74696d653a3a68696464656e5f696e636c7564653a3a64697370617463680a3a3a43616c6c61626c6543616c6c466f723c536166654d6f64652c2052756e74696d653e00140020457468657265756d0400850501b50173656c663a3a73705f6170695f68696464656e5f696e636c756465735f636f6e7374727563745f72756e74696d653a3a68696464656e5f696e636c7564653a3a64697370617463680a3a3a43616c6c61626c6543616c6c466f723c457468657265756d2c2052756e74696d653e0015000c45564d0400ad0501a10173656c663a3a73705f6170695f68696464656e5f696e636c756465735f636f6e7374727563745f72756e74696d653a3a68696464656e5f696e636c7564653a3a64697370617463680a3a3a43616c6c61626c6543616c6c466f723c45564d2c2052756e74696d653e0016002844796e616d69634665650400c10501bd0173656c663a3a73705f6170695f68696464656e5f696e636c756465735f636f6e7374727563745f72756e74696d653a3a68696464656e5f696e636c7564653a3a64697370617463680a3a3a43616c6c61626c6543616c6c466f723c44796e616d69634665652c2052756e74696d653e0018001c426173654665650400c50501b10173656c663a3a73705f6170695f68696464656e5f696e636c756465735f636f6e7374727563745f72756e74696d653a3a68696464656e5f696e636c7564653a3a64697370617463680a3a3a43616c6c61626c6543616c6c466f723c426173654665652c2052756e74696d653e001900144472616e640400c90501a90173656c663a3a73705f6170695f68696464656e5f696e636c756465735f636f6e7374727563745f72756e74696d653a3a68696464656e5f696e636c7564653a3a64697370617463680a3a3a43616c6c61626c6543616c6c466f723c4472616e642c2052756e74696d653e001a000019030c4470616c6c65745f636f6c6c6563746976651870616c6c65741043616c6c0804540004490001182c7365745f6d656d626572730c012c6e65775f6d656d626572735d0201445665633c543a3a4163636f756e7449643e0001147072696d65a801504f7074696f6e3c543a3a4163636f756e7449643e0001246f6c645f636f756e7410012c4d656d626572436f756e74000060805365742074686520636f6c6c6563746976652773206d656d626572736869702e0045012d20606e65775f6d656d62657273603a20546865206e6577206d656d626572206c6973742e204265206e69636520746f2074686520636861696e20616e642070726f7669646520697420736f727465642ee02d20607072696d65603a20546865207072696d65206d656d6265722077686f736520766f74652073657473207468652064656661756c742e59012d20606f6c645f636f756e74603a2054686520757070657220626f756e6420666f72207468652070726576696f7573206e756d626572206f66206d656d6265727320696e2073746f726167652e205573656420666f7250202077656967687420657374696d6174696f6e2e00d4546865206469737061746368206f6620746869732063616c6c206d75737420626520605365744d656d626572734f726967696e602e0051014e4f54453a20446f6573206e6f7420656e666f7263652074686520657870656374656420604d61784d656d6265727360206c696d6974206f6e2074686520616d6f756e74206f66206d656d626572732c2062757421012020202020207468652077656967687420657374696d6174696f6e732072656c79206f6e20697420746f20657374696d61746520646973706174636861626c65207765696768742e002823205741524e494e473a005901546865206070616c6c65742d636f6c6c656374697665602063616e20616c736f206265206d616e61676564206279206c6f676963206f757473696465206f66207468652070616c6c6574207468726f75676820746865b8696d706c656d656e746174696f6e206f6620746865207472616974205b604368616e67654d656d62657273605d2e5501416e792063616c6c20746f20607365745f6d656d6265727360206d757374206265206361726566756c207468617420746865206d656d6265722073657420646f65736e277420676574206f7574206f662073796e63a477697468206f74686572206c6f676963206d616e6167696e6720746865206d656d626572207365742e0038232320436f6d706c65786974793a502d20604f284d50202b204e29602077686572653ae020202d20604d60206f6c642d6d656d626572732d636f756e742028636f64652d20616e6420676f7665726e616e63652d626f756e64656429e020202d20604e60206e65772d6d656d626572732d636f756e742028636f64652d20616e6420676f7665726e616e63652d626f756e646564299820202d206050602070726f706f73616c732d636f756e742028636f64652d626f756e646564291c6578656375746508012070726f706f73616c1503017c426f783c3c5420617320436f6e6669673c493e3e3a3a50726f706f73616c3e0001306c656e6774685f626f756e646101010c753332000124f0446973706174636820612070726f706f73616c2066726f6d2061206d656d626572207573696e672074686520604d656d62657260206f726967696e2e00a84f726967696e206d7573742062652061206d656d626572206f662074686520636f6c6c6563746976652e0038232320436f6d706c65786974793a5c2d20604f2842202b204d202b205029602077686572653ad82d20604260206973206070726f706f73616c602073697a6520696e20627974657320286c656e6774682d6665652d626f756e64656429882d20604d60206d656d626572732d636f756e742028636f64652d626f756e64656429a82d2060506020636f6d706c6578697479206f66206469737061746368696e67206070726f706f73616c601c70726f706f73650c012070726f706f73616c1503017c426f783c3c5420617320436f6e6669673c493e3e3a3a50726f706f73616c3e0001306c656e6774685f626f756e646101010c7533320001206475726174696f6e100144426c6f636b4e756d626572466f723c543e000238f84164642061206e65772070726f706f73616c20746f2065697468657220626520766f746564206f6e206f72206578656375746564206469726563746c792e00845265717569726573207468652073656e64657220746f206265206d656d6265722e004101607468726573686f6c64602064657465726d696e65732077686574686572206070726f706f73616c60206973206578656375746564206469726563746c792028607468726573686f6c64203c20326029546f722070757420757020666f7220766f74696e672e0034232320436f6d706c6578697479ac2d20604f2842202b204d202b2050312960206f7220604f2842202b204d202b20503229602077686572653ae020202d20604260206973206070726f706f73616c602073697a6520696e20627974657320286c656e6774682d6665652d626f756e64656429dc20202d20604d60206973206d656d626572732d636f756e742028636f64652d20616e6420676f7665726e616e63652d626f756e64656429c420202d206272616e6368696e6720697320696e666c75656e63656420627920607468726573686f6c64602077686572653af4202020202d20605031602069732070726f706f73616c20657865637574696f6e20636f6d706c65786974792028607468726573686f6c64203c20326029fc202020202d20605032602069732070726f706f73616c732d636f756e742028636f64652d626f756e646564292028607468726573686f6c64203e3d2032602910766f74650c012070726f706f73616c34011c543a3a48617368000114696e6465786101013450726f706f73616c496e64657800011c617070726f7665240110626f6f6c000324f041646420616e20617965206f72206e617920766f746520666f72207468652073656e64657220746f2074686520676976656e2070726f706f73616c2e008c5265717569726573207468652073656e64657220746f2062652061206d656d6265722e0049015472616e73616374696f6e20666565732077696c6c2062652077616976656420696620746865206d656d62657220697320766f74696e67206f6e20616e7920706172746963756c61722070726f706f73616c5101666f72207468652066697273742074696d6520616e64207468652063616c6c206973207375636365737366756c2e2053756273657175656e7420766f7465206368616e6765732077696c6c206368617267652061106665652e34232320436f6d706c657869747909012d20604f284d296020776865726520604d60206973206d656d626572732d636f756e742028636f64652d20616e6420676f7665726e616e63652d626f756e646564294c646973617070726f76655f70726f706f73616c04013470726f706f73616c5f6861736834011c543a3a486173680005285901446973617070726f766520612070726f706f73616c2c20636c6f73652c20616e642072656d6f76652069742066726f6d207468652073797374656d2c207265676172646c657373206f66206974732063757272656e741873746174652e00884d7573742062652063616c6c65642062792074686520526f6f74206f726967696e2e002c506172616d65746572733a1d012a206070726f706f73616c5f68617368603a205468652068617368206f66207468652070726f706f73616c20746861742073686f756c6420626520646973617070726f7665642e0034232320436f6d706c6578697479ac4f285029207768657265205020697320746865206e756d626572206f66206d61782070726f706f73616c7314636c6f736510013470726f706f73616c5f6861736834011c543a3a48617368000114696e6465786101013450726f706f73616c496e64657800015470726f706f73616c5f7765696768745f626f756e642c01185765696768740001306c656e6774685f626f756e646101010c7533320006604d01436c6f7365206120766f746520746861742069732065697468657220617070726f7665642c20646973617070726f766564206f722077686f736520766f74696e6720706572696f642068617320656e6465642e0055014d61792062652063616c6c656420627920616e79207369676e6564206163636f756e7420696e206f7264657220746f2066696e69736820766f74696e6720616e6420636c6f7365207468652070726f706f73616c2e00490149662063616c6c6564206265666f72652074686520656e64206f662074686520766f74696e6720706572696f642069742077696c6c206f6e6c7920636c6f73652074686520766f7465206966206974206973bc68617320656e6f75676820766f74657320746f20626520617070726f766564206f7220646973617070726f7665642e00490149662063616c6c65642061667465722074686520656e64206f662074686520766f74696e6720706572696f642061627374656e74696f6e732061726520636f756e7465642061732072656a656374696f6e732501756e6c6573732074686572652069732061207072696d65206d656d6265722073657420616e6420746865207072696d65206d656d626572206361737420616e20617070726f76616c2e00610149662074686520636c6f7365206f7065726174696f6e20636f6d706c65746573207375636365737366756c6c79207769746820646973617070726f76616c2c20746865207472616e73616374696f6e206665652077696c6c5d016265207761697665642e204f746865727769736520657865637574696f6e206f662074686520617070726f766564206f7065726174696f6e2077696c6c206265206368617267656420746f207468652063616c6c65722e0061012b206070726f706f73616c5f7765696768745f626f756e64603a20546865206d6178696d756d20616d6f756e74206f662077656967687420636f6e73756d656420627920657865637574696e672074686520636c6f7365642470726f706f73616c2e61012b20606c656e6774685f626f756e64603a2054686520757070657220626f756e6420666f7220746865206c656e677468206f66207468652070726f706f73616c20696e2073746f726167652e20436865636b65642076696135016073746f726167653a3a726561646020736f206974206973206073697a655f6f663a3a3c7533323e2829203d3d203460206c6172676572207468616e207468652070757265206c656e6774682e0034232320436f6d706c6578697479742d20604f2842202b204d202b205031202b20503229602077686572653ae020202d20604260206973206070726f706f73616c602073697a6520696e20627974657320286c656e6774682d6665652d626f756e64656429dc20202d20604d60206973206d656d626572732d636f756e742028636f64652d20616e6420676f7665726e616e63652d626f756e64656429c820202d20605031602069732074686520636f6d706c6578697479206f66206070726f706f73616c6020707265696d6167652ea420202d20605032602069732070726f706f73616c2d636f756e742028636f64652d626f756e64656429040d01436f6e7461696e7320612076617269616e742070657220646973706174636861626c652065787472696e736963207468617420746869732070616c6c6574206861732e1d030c4470616c6c65745f6d656d626572736869701870616c6c65741043616c6c08045400044900011c286164645f6d656d62657204010c77686f550201504163636f756e7449644c6f6f6b75704f663c543e00000c784164642061206d656d626572206077686f6020746f20746865207365742e009c4d6179206f6e6c792062652063616c6c65642066726f6d2060543a3a4164644f726967696e602e3472656d6f76655f6d656d62657204010c77686f550201504163636f756e7449644c6f6f6b75704f663c543e00010c8c52656d6f76652061206d656d626572206077686f602066726f6d20746865207365742e00a84d6179206f6e6c792062652063616c6c65642066726f6d2060543a3a52656d6f76654f726967696e602e2c737761705f6d656d62657208011872656d6f7665550201504163636f756e7449644c6f6f6b75704f663c543e00010c616464550201504163636f756e7449644c6f6f6b75704f663c543e000214bc53776170206f7574206f6e65206d656d626572206072656d6f76656020666f7220616e6f746865722060616464602e00a04d6179206f6e6c792062652063616c6c65642066726f6d2060543a3a537761704f726967696e602e000d015072696d65206d656d62657273686970206973202a6e6f742a207061737365642066726f6d206072656d6f76656020746f2060616464602c20696620657874616e742e3472657365745f6d656d6265727304011c6d656d626572735d0201445665633c543a3a4163636f756e7449643e00031055014368616e676520746865206d656d6265727368697020746f2061206e6577207365742c20646973726567617264696e6720746865206578697374696e67206d656d626572736869702e204265206e69636520616e64687061737320606d656d6265727360207072652d736f727465642e00a44d6179206f6e6c792062652063616c6c65642066726f6d2060543a3a52657365744f726967696e602e286368616e67655f6b657904010c6e6577550201504163636f756e7449644c6f6f6b75704f663c543e000414d453776170206f7574207468652073656e64696e67206d656d62657220666f7220736f6d65206f74686572206b657920606e6577602e00f04d6179206f6e6c792062652063616c6c65642066726f6d20605369676e656460206f726967696e206f6620612063757272656e74206d656d6265722e001d015072696d65206d656d62657273686970206973207061737365642066726f6d20746865206f726967696e206163636f756e7420746f20606e6577602c20696620657874616e742e247365745f7072696d6504010c77686f550201504163636f756e7449644c6f6f6b75704f663c543e00050cbc53657420746865207072696d65206d656d6265722e204d75737420626520612063757272656e74206d656d6265722e00a44d6179206f6e6c792062652063616c6c65642066726f6d2060543a3a5072696d654f726967696e602e2c636c6561725f7072696d6500060c9452656d6f766520746865207072696d65206d656d626572206966206974206578697374732e00a44d6179206f6e6c792062652063616c6c65642066726f6d2060543a3a5072696d654f726967696e602e040d01436f6e7461696e7320612076617269616e742070657220646973706174636861626c652065787472696e736963207468617420746869732070616c6c6574206861732e21030c4470616c6c65745f6d656d626572736869701870616c6c65741043616c6c08045400044900011c286164645f6d656d62657204010c77686f550201504163636f756e7449644c6f6f6b75704f663c543e00000c784164642061206d656d626572206077686f6020746f20746865207365742e009c4d6179206f6e6c792062652063616c6c65642066726f6d2060543a3a4164644f726967696e602e3472656d6f76655f6d656d62657204010c77686f550201504163636f756e7449644c6f6f6b75704f663c543e00010c8c52656d6f76652061206d656d626572206077686f602066726f6d20746865207365742e00a84d6179206f6e6c792062652063616c6c65642066726f6d2060543a3a52656d6f76654f726967696e602e2c737761705f6d656d62657208011872656d6f7665550201504163636f756e7449644c6f6f6b75704f663c543e00010c616464550201504163636f756e7449644c6f6f6b75704f663c543e000214bc53776170206f7574206f6e65206d656d626572206072656d6f76656020666f7220616e6f746865722060616464602e00a04d6179206f6e6c792062652063616c6c65642066726f6d2060543a3a537761704f726967696e602e000d015072696d65206d656d62657273686970206973202a6e6f742a207061737365642066726f6d206072656d6f76656020746f2060616464602c20696620657874616e742e3472657365745f6d656d6265727304011c6d656d626572735d0201445665633c543a3a4163636f756e7449643e00031055014368616e676520746865206d656d6265727368697020746f2061206e6577207365742c20646973726567617264696e6720746865206578697374696e67206d656d626572736869702e204265206e69636520616e64687061737320606d656d6265727360207072652d736f727465642e00a44d6179206f6e6c792062652063616c6c65642066726f6d2060543a3a52657365744f726967696e602e286368616e67655f6b657904010c6e6577550201504163636f756e7449644c6f6f6b75704f663c543e000414d453776170206f7574207468652073656e64696e67206d656d62657220666f7220736f6d65206f74686572206b657920606e6577602e00f04d6179206f6e6c792062652063616c6c65642066726f6d20605369676e656460206f726967696e206f6620612063757272656e74206d656d6265722e001d015072696d65206d656d62657273686970206973207061737365642066726f6d20746865206f726967696e206163636f756e7420746f20606e6577602c20696620657874616e742e247365745f7072696d6504010c77686f550201504163636f756e7449644c6f6f6b75704f663c543e00050cbc53657420746865207072696d65206d656d6265722e204d75737420626520612063757272656e74206d656d6265722e00a44d6179206f6e6c792062652063616c6c65642066726f6d2060543a3a5072696d654f726967696e602e2c636c6561725f7072696d6500060c9452656d6f766520746865207072696d65206d656d626572206966206974206578697374732e00a44d6179206f6e6c792062652063616c6c65642066726f6d2060543a3a5072696d654f726967696e602e040d01436f6e7461696e7320612076617269616e742070657220646973706174636861626c652065787472696e736963207468617420746869732070616c6c6574206861732e25030c3870616c6c65745f7574696c6974791870616c6c65741043616c6c04045400011814626174636804011463616c6c732903017c5665633c3c5420617320436f6e6669673e3a3a52756e74696d6543616c6c3e0000487c53656e642061206261746368206f662064697370617463682063616c6c732e00b04d61792062652063616c6c65642066726f6d20616e79206f726967696e2065786365707420604e6f6e65602e005d012d206063616c6c73603a205468652063616c6c7320746f20626520646973706174636865642066726f6d207468652073616d65206f726967696e2e20546865206e756d626572206f662063616c6c206d757374206e6f74390120206578636565642074686520636f6e7374616e743a2060626174636865645f63616c6c735f6c696d6974602028617661696c61626c6520696e20636f6e7374616e74206d65746164617461292e0055014966206f726967696e20697320726f6f74207468656e207468652063616c6c7320617265206469737061746368656420776974686f757420636865636b696e67206f726967696e2066696c7465722e202854686973ec696e636c7564657320627970617373696e6720606672616d655f73797374656d3a3a436f6e6669673a3a4261736543616c6c46696c74657260292e0034232320436f6d706c6578697479d02d204f284329207768657265204320697320746865206e756d626572206f662063616c6c7320746f20626520626174636865642e005501546869732077696c6c2072657475726e20604f6b6020696e20616c6c2063697263756d7374616e6365732e20546f2064657465726d696e65207468652073756363657373206f66207468652062617463682c20616e31016576656e74206973206465706f73697465642e20496620612063616c6c206661696c656420616e64207468652062617463682077617320696e7465727275707465642c207468656e207468655501604261746368496e74657272757074656460206576656e74206973206465706f73697465642c20616c6f6e67207769746820746865206e756d626572206f66207375636365737366756c2063616c6c73206d6164654d01616e6420746865206572726f72206f6620746865206661696c65642063616c6c2e20496620616c6c2077657265207375636365737366756c2c207468656e2074686520604261746368436f6d706c65746564604c6576656e74206973206465706f73697465642e3461735f64657269766174697665080114696e6465789c010c75313600011063616c6c1503017c426f783c3c5420617320436f6e6669673e3a3a52756e74696d6543616c6c3e000134dc53656e6420612063616c6c207468726f75676820616e20696e64657865642070736575646f6e796d206f66207468652073656e6465722e00550146696c7465722066726f6d206f726967696e206172652070617373656420616c6f6e672e205468652063616c6c2077696c6c2062652064697370617463686564207769746820616e206f726967696e207768696368bc757365207468652073616d652066696c74657220617320746865206f726967696e206f6620746869732063616c6c2e0045014e4f54453a20496620796f75206e65656420746f20656e73757265207468617420616e79206163636f756e742d62617365642066696c746572696e67206973206e6f7420686f6e6f7265642028692e652e61016265636175736520796f7520657870656374206070726f78796020746f2068617665206265656e2075736564207072696f7220696e207468652063616c6c20737461636b20616e6420796f7520646f206e6f742077616e7451017468652063616c6c207265737472696374696f6e7320746f206170706c7920746f20616e79207375622d6163636f756e7473292c207468656e20757365206061735f6d756c74695f7468726573686f6c645f31607c696e20746865204d756c74697369672070616c6c657420696e73746561642e00f44e4f54453a205072696f7220746f2076657273696f6e202a31322c2074686973207761732063616c6c6564206061735f6c696d697465645f737562602e00cc546865206469737061746368206f726967696e20666f7220746869732063616c6c206d757374206265205f5369676e65645f2e2462617463685f616c6c04011463616c6c732903017c5665633c3c5420617320436f6e6669673e3a3a52756e74696d6543616c6c3e000234ec53656e642061206261746368206f662064697370617463682063616c6c7320616e642061746f6d6963616c6c792065786563757465207468656d2e21015468652077686f6c65207472616e73616374696f6e2077696c6c20726f6c6c6261636b20616e64206661696c20696620616e79206f66207468652063616c6c73206661696c65642e00b04d61792062652063616c6c65642066726f6d20616e79206f726967696e2065786365707420604e6f6e65602e005d012d206063616c6c73603a205468652063616c6c7320746f20626520646973706174636865642066726f6d207468652073616d65206f726967696e2e20546865206e756d626572206f662063616c6c206d757374206e6f74390120206578636565642074686520636f6e7374616e743a2060626174636865645f63616c6c735f6c696d6974602028617661696c61626c6520696e20636f6e7374616e74206d65746164617461292e0055014966206f726967696e20697320726f6f74207468656e207468652063616c6c7320617265206469737061746368656420776974686f757420636865636b696e67206f726967696e2066696c7465722e202854686973ec696e636c7564657320627970617373696e6720606672616d655f73797374656d3a3a436f6e6669673a3a4261736543616c6c46696c74657260292e0034232320436f6d706c6578697479d02d204f284329207768657265204320697320746865206e756d626572206f662063616c6c7320746f20626520626174636865642e2c64697370617463685f617308012461735f6f726967696e2d030154426f783c543a3a50616c6c6574734f726967696e3e00011063616c6c1503017c426f783c3c5420617320436f6e6669673e3a3a52756e74696d6543616c6c3e000318c84469737061746368657320612066756e6374696f6e2063616c6c207769746820612070726f7669646564206f726967696e2e00c4546865206469737061746368206f726967696e20666f7220746869732063616c6c206d757374206265205f526f6f745f2e0034232320436f6d706c65786974791c2d204f2831292e2c666f7263655f626174636804011463616c6c732903017c5665633c3c5420617320436f6e6669673e3a3a52756e74696d6543616c6c3e0004347c53656e642061206261746368206f662064697370617463682063616c6c732ed4556e6c696b6520606261746368602c20697420616c6c6f7773206572726f727320616e6420776f6e277420696e746572727570742e00b04d61792062652063616c6c65642066726f6d20616e79206f726967696e2065786365707420604e6f6e65602e005d012d206063616c6c73603a205468652063616c6c7320746f20626520646973706174636865642066726f6d207468652073616d65206f726967696e2e20546865206e756d626572206f662063616c6c206d757374206e6f74390120206578636565642074686520636f6e7374616e743a2060626174636865645f63616c6c735f6c696d6974602028617661696c61626c6520696e20636f6e7374616e74206d65746164617461292e004d014966206f726967696e20697320726f6f74207468656e207468652063616c6c732061726520646973706174636820776974686f757420636865636b696e67206f726967696e2066696c7465722e202854686973ec696e636c7564657320627970617373696e6720606672616d655f73797374656d3a3a436f6e6669673a3a4261736543616c6c46696c74657260292e0034232320436f6d706c6578697479d02d204f284329207768657265204320697320746865206e756d626572206f662063616c6c7320746f20626520626174636865642e2c776974685f77656967687408011063616c6c1503017c426f783c3c5420617320436f6e6669673e3a3a52756e74696d6543616c6c3e0001187765696768742c0118576569676874000518c4446973706174636820612066756e6374696f6e2063616c6c2077697468206120737065636966696564207765696768742e002d01546869732066756e6374696f6e20646f6573206e6f7420636865636b2074686520776569676874206f66207468652063616c6c2c20616e6420696e737465616420616c6c6f777320746865b8526f6f74206f726967696e20746f20737065636966792074686520776569676874206f66207468652063616c6c2e00c4546865206469737061746368206f726967696e20666f7220746869732063616c6c206d757374206265205f526f6f745f2e040d01436f6e7461696e7320612076617269616e742070657220646973706174636861626c652065787472696e736963207468617420746869732070616c6c6574206861732e29030000021503002d0308586e6f64655f73756274656e736f725f72756e74696d65304f726967696e43616c6c65720001101873797374656d0400310301746672616d655f73797374656d3a3a4f726967696e3c52756e74696d653e0000002c547269756d7669726174650400350301010170616c6c65745f636f6c6c6563746976653a3a4f726967696e3c52756e74696d652c2070616c6c65745f636f6c6c6563746976653a3a496e7374616e6365313e00080020457468657265756d04003903015c70616c6c65745f657468657265756d3a3a4f726967696e00150010566f69640400010201410173656c663a3a73705f6170695f68696464656e5f696e636c756465735f636f6e7374727563745f72756e74696d653a3a68696464656e5f696e636c7564653a3a0a5f5f707269766174653a3a566f69640003000031030c346672616d655f737570706f7274206469737061746368245261774f726967696e04244163636f756e7449640100010c10526f6f74000000185369676e656404000001244163636f756e744964000100104e6f6e65000200003503084470616c6c65745f636f6c6c656374697665245261774f726967696e08244163636f756e7449640100044900010c1c4d656d62657273080010012c4d656d626572436f756e74000010012c4d656d626572436f756e74000000184d656d62657204000001244163636f756e744964000100205f5068616e746f6d000200003903083c70616c6c65745f657468657265756d245261774f726967696e0001044c457468657265756d5472616e73616374696f6e04000d01011048313630000000003d030c2c70616c6c65745f7375646f1870616c6c65741043616c6c040454000114107375646f04011063616c6c1503017c426f783c3c5420617320436f6e6669673e3a3a52756e74696d6543616c6c3e000004350141757468656e7469636174657320746865207375646f206b657920616e64206469737061746368657320612066756e6374696f6e2063616c6c20776974682060526f6f7460206f726967696e2e547375646f5f756e636865636b65645f77656967687408011063616c6c1503017c426f783c3c5420617320436f6e6669673e3a3a52756e74696d6543616c6c3e0001187765696768742c0118576569676874000114350141757468656e7469636174657320746865207375646f206b657920616e64206469737061746368657320612066756e6374696f6e2063616c6c20776974682060526f6f7460206f726967696e2e2d01546869732066756e6374696f6e20646f6573206e6f7420636865636b2074686520776569676874206f66207468652063616c6c2c20616e6420696e737465616420616c6c6f777320746865b05375646f207573657220746f20737065636966792074686520776569676874206f66207468652063616c6c2e00cc546865206469737061746368206f726967696e20666f7220746869732063616c6c206d757374206265205f5369676e65645f2e1c7365745f6b657904010c6e6577550201504163636f756e7449644c6f6f6b75704f663c543e0002085d0141757468656e74696361746573207468652063757272656e74207375646f206b657920616e6420736574732074686520676976656e204163636f756e7449642028606e6577602920617320746865206e6577207375646f106b65792e1c7375646f5f617308010c77686f550201504163636f756e7449644c6f6f6b75704f663c543e00011063616c6c1503017c426f783c3c5420617320436f6e6669673e3a3a52756e74696d6543616c6c3e0003104d0141757468656e7469636174657320746865207375646f206b657920616e64206469737061746368657320612066756e6374696f6e2063616c6c207769746820605369676e656460206f726967696e2066726f6d406120676976656e206163636f756e742e00cc546865206469737061746368206f726967696e20666f7220746869732063616c6c206d757374206265205f5369676e65645f2e2872656d6f76655f6b657900040c845065726d616e656e746c792072656d6f76657320746865207375646f206b65792e006c2a2a546869732063616e6e6f7420626520756e2d646f6e652e2a2a040d01436f6e7461696e7320612076617269616e742070657220646973706174636861626c652065787472696e736963207468617420746869732070616c6c6574206861732e41030c3c70616c6c65745f6d756c74697369671870616c6c65741043616c6c0404540001105061735f6d756c74695f7468726573686f6c645f310801446f746865725f7369676e61746f726965735d0201445665633c543a3a4163636f756e7449643e00011063616c6c1503017c426f783c3c5420617320436f6e6669673e3a3a52756e74696d6543616c6c3e0000305101496d6d6564696174656c792064697370617463682061206d756c74692d7369676e61747572652063616c6c207573696e6720612073696e676c6520617070726f76616c2066726f6d207468652063616c6c65722e00cc546865206469737061746368206f726967696e20666f7220746869732063616c6c206d757374206265205f5369676e65645f2e003d012d20606f746865725f7369676e61746f72696573603a20546865206163636f756e747320286f74686572207468616e207468652073656e646572292077686f206172652070617274206f662074686501016d756c74692d7369676e61747572652c2062757420646f206e6f7420706172746963697061746520696e2074686520617070726f76616c2070726f636573732e882d206063616c6c603a205468652063616c6c20746f2062652065786563757465642e00b8526573756c74206973206571756976616c656e7420746f20746865206469737061746368656420726573756c742e0034232320436f6d706c657869747919014f285a202b204329207768657265205a20697320746865206c656e677468206f66207468652063616c6c20616e6420432069747320657865637574696f6e207765696768742e2061735f6d756c74691401247468726573686f6c649c010c7531360001446f746865725f7369676e61746f726965735d0201445665633c543a3a4163636f756e7449643e00013c6d617962655f74696d65706f696e74450301904f7074696f6e3c54696d65706f696e743c426c6f636b4e756d626572466f723c543e3e3e00011063616c6c1503017c426f783c3c5420617320436f6e6669673e3a3a52756e74696d6543616c6c3e0001286d61785f7765696768742c011857656967687400019c5501526567697374657220617070726f76616c20666f72206120646973706174636820746f206265206d6164652066726f6d20612064657465726d696e697374696320636f6d706f73697465206163636f756e74206966f8617070726f766564206279206120746f74616c206f6620607468726573686f6c64202d203160206f6620606f746865725f7369676e61746f72696573602e00b049662074686572652061726520656e6f7567682c207468656e206469737061746368207468652063616c6c2e002d015061796d656e743a20604465706f73697442617365602077696c6c20626520726573657276656420696620746869732069732074686520666972737420617070726f76616c2c20706c75733d01607468726573686f6c64602074696d657320604465706f736974466163746f72602e2049742069732072657475726e6564206f6e636520746869732064697370617463682068617070656e73206f723469732063616e63656c6c65642e00cc546865206469737061746368206f726967696e20666f7220746869732063616c6c206d757374206265205f5369676e65645f2e0055012d20607468726573686f6c64603a2054686520746f74616c206e756d626572206f6620617070726f76616c7320666f722074686973206469737061746368206265666f72652069742069732065786563757465642e41012d20606f746865725f7369676e61746f72696573603a20546865206163636f756e747320286f74686572207468616e207468652073656e646572292077686f2063616e20617070726f766520746869736c64697370617463682e204d6179206e6f7420626520656d7074792e59012d20606d617962655f74696d65706f696e74603a20496620746869732069732074686520666972737420617070726f76616c2c207468656e2074686973206d75737420626520604e6f6e65602e20496620697420697351016e6f742074686520666972737420617070726f76616c2c207468656e206974206d7573742062652060536f6d65602c2077697468207468652074696d65706f696e742028626c6f636b206e756d62657220616e64d47472616e73616374696f6e20696e64657829206f662074686520666972737420617070726f76616c207472616e73616374696f6e2e882d206063616c6c603a205468652063616c6c20746f2062652065786563757465642e001d014e4f54453a20556e6c6573732074686973206973207468652066696e616c20617070726f76616c2c20796f752077696c6c2067656e6572616c6c792077616e7420746f20757365190160617070726f76655f61735f6d756c74696020696e73746561642c2073696e6365206974206f6e6c7920726571756972657320612068617368206f66207468652063616c6c2e005901526573756c74206973206571756976616c656e7420746f20746865206469737061746368656420726573756c7420696620607468726573686f6c64602069732065786163746c79206031602e204f746865727769736555016f6e20737563636573732c20726573756c7420697320604f6b6020616e642074686520726573756c742066726f6d2074686520696e746572696f722063616c6c2c206966206974207761732065786563757465642cdc6d617920626520666f756e6420696e20746865206465706f736974656420604d756c7469736967457865637574656460206576656e742e0034232320436f6d706c6578697479502d20604f2853202b205a202b2043616c6c29602ecc2d20557020746f206f6e652062616c616e63652d72657365727665206f7220756e72657365727665206f7065726174696f6e2e3d012d204f6e6520706173737468726f756768206f7065726174696f6e2c206f6e6520696e736572742c20626f746820604f285329602077686572652060536020697320746865206e756d626572206f66450120207369676e61746f726965732e206053602069732063617070656420627920604d61785369676e61746f72696573602c207769746820776569676874206265696e672070726f706f7274696f6e616c2e21012d204f6e652063616c6c20656e636f6465202620686173682c20626f7468206f6620636f6d706c657869747920604f285a296020776865726520605a602069732074782d6c656e2ebc2d204f6e6520656e636f6465202620686173682c20626f7468206f6620636f6d706c657869747920604f285329602ed42d20557020746f206f6e652062696e6172792073656172636820616e6420696e736572742028604f286c6f6753202b20532960292ef82d20492f4f3a2031207265616420604f285329602c20757020746f2031206d757461746520604f285329602e20557020746f206f6e652072656d6f76652e302d204f6e65206576656e742e6c2d2054686520776569676874206f6620746865206063616c6c602e4d012d2053746f726167653a20696e7365727473206f6e65206974656d2c2076616c75652073697a6520626f756e64656420627920604d61785369676e61746f72696573602c20776974682061206465706f7369741901202074616b656e20666f7220697473206c69666574696d65206f6620604465706f73697442617365202b207468726573686f6c64202a204465706f736974466163746f72602e40617070726f76655f61735f6d756c74691401247468726573686f6c649c010c7531360001446f746865725f7369676e61746f726965735d0201445665633c543a3a4163636f756e7449643e00013c6d617962655f74696d65706f696e74450301904f7074696f6e3c54696d65706f696e743c426c6f636b4e756d626572466f723c543e3e3e00012463616c6c5f686173680401205b75383b2033325d0001286d61785f7765696768742c01185765696768740002785501526567697374657220617070726f76616c20666f72206120646973706174636820746f206265206d6164652066726f6d20612064657465726d696e697374696320636f6d706f73697465206163636f756e74206966f8617070726f766564206279206120746f74616c206f6620607468726573686f6c64202d203160206f6620606f746865725f7369676e61746f72696573602e002d015061796d656e743a20604465706f73697442617365602077696c6c20626520726573657276656420696620746869732069732074686520666972737420617070726f76616c2c20706c75733d01607468726573686f6c64602074696d657320604465706f736974466163746f72602e2049742069732072657475726e6564206f6e636520746869732064697370617463682068617070656e73206f723469732063616e63656c6c65642e00cc546865206469737061746368206f726967696e20666f7220746869732063616c6c206d757374206265205f5369676e65645f2e0055012d20607468726573686f6c64603a2054686520746f74616c206e756d626572206f6620617070726f76616c7320666f722074686973206469737061746368206265666f72652069742069732065786563757465642e41012d20606f746865725f7369676e61746f72696573603a20546865206163636f756e747320286f74686572207468616e207468652073656e646572292077686f2063616e20617070726f766520746869736c64697370617463682e204d6179206e6f7420626520656d7074792e59012d20606d617962655f74696d65706f696e74603a20496620746869732069732074686520666972737420617070726f76616c2c207468656e2074686973206d75737420626520604e6f6e65602e20496620697420697351016e6f742074686520666972737420617070726f76616c2c207468656e206974206d7573742062652060536f6d65602c2077697468207468652074696d65706f696e742028626c6f636b206e756d62657220616e64d47472616e73616374696f6e20696e64657829206f662074686520666972737420617070726f76616c207472616e73616374696f6e2ecc2d206063616c6c5f68617368603a205468652068617368206f66207468652063616c6c20746f2062652065786563757465642e0035014e4f54453a2049662074686973206973207468652066696e616c20617070726f76616c2c20796f752077696c6c2077616e7420746f20757365206061735f6d756c74696020696e73746561642e0034232320436f6d706c6578697479242d20604f285329602ecc2d20557020746f206f6e652062616c616e63652d72657365727665206f7220756e72657365727665206f7065726174696f6e2e3d012d204f6e6520706173737468726f756768206f7065726174696f6e2c206f6e6520696e736572742c20626f746820604f285329602077686572652060536020697320746865206e756d626572206f66450120207369676e61746f726965732e206053602069732063617070656420627920604d61785369676e61746f72696573602c207769746820776569676874206265696e672070726f706f7274696f6e616c2ebc2d204f6e6520656e636f6465202620686173682c20626f7468206f6620636f6d706c657869747920604f285329602ed42d20557020746f206f6e652062696e6172792073656172636820616e6420696e736572742028604f286c6f6753202b20532960292ef82d20492f4f3a2031207265616420604f285329602c20757020746f2031206d757461746520604f285329602e20557020746f206f6e652072656d6f76652e302d204f6e65206576656e742e4d012d2053746f726167653a20696e7365727473206f6e65206974656d2c2076616c75652073697a6520626f756e64656420627920604d61785369676e61746f72696573602c20776974682061206465706f7369741901202074616b656e20666f7220697473206c69666574696d65206f6620604465706f73697442617365202b207468726573686f6c64202a204465706f736974466163746f72602e3c63616e63656c5f61735f6d756c74691001247468726573686f6c649c010c7531360001446f746865725f7369676e61746f726965735d0201445665633c543a3a4163636f756e7449643e00012474696d65706f696e74d8017054696d65706f696e743c426c6f636b4e756d626572466f723c543e3e00012463616c6c5f686173680401205b75383b2033325d000354550143616e63656c2061207072652d6578697374696e672c206f6e2d676f696e67206d756c7469736967207472616e73616374696f6e2e20416e79206465706f7369742072657365727665642070726576696f75736c79c4666f722074686973206f7065726174696f6e2077696c6c20626520756e7265736572766564206f6e20737563636573732e00cc546865206469737061746368206f726967696e20666f7220746869732063616c6c206d757374206265205f5369676e65645f2e0055012d20607468726573686f6c64603a2054686520746f74616c206e756d626572206f6620617070726f76616c7320666f722074686973206469737061746368206265666f72652069742069732065786563757465642e41012d20606f746865725f7369676e61746f72696573603a20546865206163636f756e747320286f74686572207468616e207468652073656e646572292077686f2063616e20617070726f766520746869736c64697370617463682e204d6179206e6f7420626520656d7074792e5d012d206074696d65706f696e74603a205468652074696d65706f696e742028626c6f636b206e756d62657220616e64207472616e73616374696f6e20696e64657829206f662074686520666972737420617070726f76616c787472616e73616374696f6e20666f7220746869732064697370617463682ecc2d206063616c6c5f68617368603a205468652068617368206f66207468652063616c6c20746f2062652065786563757465642e0034232320436f6d706c6578697479242d20604f285329602ecc2d20557020746f206f6e652062616c616e63652d72657365727665206f7220756e72657365727665206f7065726174696f6e2e3d012d204f6e6520706173737468726f756768206f7065726174696f6e2c206f6e6520696e736572742c20626f746820604f285329602077686572652060536020697320746865206e756d626572206f66450120207369676e61746f726965732e206053602069732063617070656420627920604d61785369676e61746f72696573602c207769746820776569676874206265696e672070726f706f7274696f6e616c2ebc2d204f6e6520656e636f6465202620686173682c20626f7468206f6620636f6d706c657869747920604f285329602e302d204f6e65206576656e742e842d20492f4f3a2031207265616420604f285329602c206f6e652072656d6f76652e702d2053746f726167653a2072656d6f766573206f6e65206974656d2e040d01436f6e7461696e7320612076617269616e742070657220646973706174636861626c652065787472696e736963207468617420746869732070616c6c6574206861732e450304184f7074696f6e04045401d80108104e6f6e6500000010536f6d650400d8000001000049030c3c70616c6c65745f707265696d6167651870616c6c65741043616c6c040454000114346e6f74655f707265696d616765040114627974657338011c5665633c75383e000010745265676973746572206120707265696d616765206f6e2d636861696e2e00550149662074686520707265696d616765207761732070726576696f75736c79207265717565737465642c206e6f2066656573206f72206465706f73697473206172652074616b656e20666f722070726f766964696e67550174686520707265696d6167652e204f74686572776973652c2061206465706f7369742069732074616b656e2070726f706f7274696f6e616c20746f207468652073697a65206f662074686520707265696d6167652e3c756e6e6f74655f707265696d6167650401106861736834011c543a3a48617368000118dc436c65617220616e20756e72657175657374656420707265696d6167652066726f6d207468652072756e74696d652073746f726167652e00fc496620606c656e602069732070726f76696465642c207468656e2069742077696c6c2062652061206d7563682063686561706572206f7065726174696f6e2e0001012d206068617368603a205468652068617368206f662074686520707265696d61676520746f2062652072656d6f7665642066726f6d207468652073746f72652eb82d20606c656e603a20546865206c656e677468206f662074686520707265696d616765206f66206068617368602e40726571756573745f707265696d6167650401106861736834011c543a3a48617368000210410152657175657374206120707265696d6167652062652075706c6f6164656420746f2074686520636861696e20776974686f757420706179696e6720616e792066656573206f72206465706f736974732e00550149662074686520707265696d6167652072657175657374732068617320616c7265616479206265656e2070726f7669646564206f6e2d636861696e2c20776520756e7265736572766520616e79206465706f7369743901612075736572206d6179206861766520706169642c20616e642074616b652074686520636f6e74726f6c206f662074686520707265696d616765206f7574206f662074686569722068616e64732e48756e726571756573745f707265696d6167650401106861736834011c543a3a4861736800030cbc436c65617220612070726576696f75736c79206d616465207265717565737420666f72206120707265696d6167652e002d014e4f54453a2054484953204d555354204e4f542042452043414c4c4544204f4e20606861736860204d4f52452054494d4553205448414e2060726571756573745f707265696d616765602e38656e737572655f75706461746564040118686173686573b401305665633c543a3a486173683e00040cc4456e7375726520746861742074686520612062756c6b206f66207072652d696d616765732069732075706772616465642e003d015468652063616c6c65722070617973206e6f20666565206966206174206c6561737420393025206f66207072652d696d616765732077657265207375636365737366756c6c7920757064617465642e040d01436f6e7461696e7320612076617269616e742070657220646973706174636861626c652065787472696e736963207468617420746869732070616c6c6574206861732e4d030c4070616c6c65745f7363686564756c65721870616c6c65741043616c6c040454000128207363686564756c651001107768656e100144426c6f636b4e756d626572466f723c543e0001386d617962655f706572696f646963510301ac4f7074696f6e3c7363686564756c653a3a506572696f643c426c6f636b4e756d626572466f723c543e3e3e0001207072696f726974790801487363686564756c653a3a5072696f7269747900011063616c6c1503017c426f783c3c5420617320436f6e6669673e3a3a52756e74696d6543616c6c3e00000470416e6f6e796d6f75736c79207363686564756c652061207461736b2e1863616e63656c0801107768656e100144426c6f636b4e756d626572466f723c543e000114696e64657810010c7533320001049443616e63656c20616e20616e6f6e796d6f75736c79207363686564756c6564207461736b2e387363686564756c655f6e616d656414010869640401205461736b4e616d650001107768656e100144426c6f636b4e756d626572466f723c543e0001386d617962655f706572696f646963510301ac4f7074696f6e3c7363686564756c653a3a506572696f643c426c6f636b4e756d626572466f723c543e3e3e0001207072696f726974790801487363686564756c653a3a5072696f7269747900011063616c6c1503017c426f783c3c5420617320436f6e6669673e3a3a52756e74696d6543616c6c3e000204585363686564756c652061206e616d6564207461736b2e3063616e63656c5f6e616d656404010869640401205461736b4e616d650003047843616e63656c2061206e616d6564207363686564756c6564207461736b2e387363686564756c655f61667465721001146166746572100144426c6f636b4e756d626572466f723c543e0001386d617962655f706572696f646963510301ac4f7074696f6e3c7363686564756c653a3a506572696f643c426c6f636b4e756d626572466f723c543e3e3e0001207072696f726974790801487363686564756c653a3a5072696f7269747900011063616c6c1503017c426f783c3c5420617320436f6e6669673e3a3a52756e74696d6543616c6c3e000404a8416e6f6e796d6f75736c79207363686564756c652061207461736b20616674657220612064656c61792e507363686564756c655f6e616d65645f616674657214010869640401205461736b4e616d650001146166746572100144426c6f636b4e756d626572466f723c543e0001386d617962655f706572696f646963510301ac4f7074696f6e3c7363686564756c653a3a506572696f643c426c6f636b4e756d626572466f723c543e3e3e0001207072696f726974790801487363686564756c653a3a5072696f7269747900011063616c6c1503017c426f783c3c5420617320436f6e6669673e3a3a52756e74696d6543616c6c3e000504905363686564756c652061206e616d6564207461736b20616674657220612064656c61792e247365745f72657472790c01107461736be401785461736b416464726573733c426c6f636b4e756d626572466f723c543e3e00011c726574726965730801087538000118706572696f64100144426c6f636b4e756d626572466f723c543e0006305901536574206120726574727920636f6e66696775726174696f6e20666f722061207461736b20736f20746861742c20696e206361736520697473207363686564756c65642072756e206661696c732c2069742077696c6c5501626520726574726965642061667465722060706572696f646020626c6f636b732c20666f72206120746f74616c20616d6f756e74206f66206072657472696573602072657472696573206f7220756e74696c2069742473756363656564732e0055015461736b73207768696368206e65656420746f206265207363686564756c656420666f72206120726574727920617265207374696c6c207375626a65637420746f20776569676874206d65746572696e6720616e6451016167656e64612073706163652c2073616d65206173206120726567756c6172207461736b2e204966206120706572696f646963207461736b206661696c732c2069742077696c6c206265207363686564756c6564906e6f726d616c6c79207768696c6520746865207461736b206973207265747279696e672e0051015461736b73207363686564756c6564206173206120726573756c74206f66206120726574727920666f72206120706572696f646963207461736b2061726520756e6e616d65642c206e6f6e2d706572696f6469633d01636c6f6e6573206f6620746865206f726967696e616c207461736b2e20546865697220726574727920636f6e66696775726174696f6e2077696c6c20626520646572697665642066726f6d207468654d016f726967696e616c207461736b277320636f6e66696775726174696f6e2c206275742077696c6c20686176652061206c6f7765722076616c756520666f72206072656d61696e696e6760207468616e20746865646f726967696e616c2060746f74616c5f72657472696573602e3c7365745f72657472795f6e616d65640c010869640401205461736b4e616d6500011c726574726965730801087538000118706572696f64100144426c6f636b4e756d626572466f723c543e0007305d01536574206120726574727920636f6e66696775726174696f6e20666f722061206e616d6564207461736b20736f20746861742c20696e206361736520697473207363686564756c65642072756e206661696c732c2069745d0177696c6c20626520726574726965642061667465722060706572696f646020626c6f636b732c20666f72206120746f74616c20616d6f756e74206f66206072657472696573602072657472696573206f7220756e74696c3069742073756363656564732e0055015461736b73207768696368206e65656420746f206265207363686564756c656420666f72206120726574727920617265207374696c6c207375626a65637420746f20776569676874206d65746572696e6720616e6451016167656e64612073706163652c2073616d65206173206120726567756c6172207461736b2e204966206120706572696f646963207461736b206661696c732c2069742077696c6c206265207363686564756c6564906e6f726d616c6c79207768696c6520746865207461736b206973207265747279696e672e0051015461736b73207363686564756c6564206173206120726573756c74206f66206120726574727920666f72206120706572696f646963207461736b2061726520756e6e616d65642c206e6f6e2d706572696f6469633d01636c6f6e6573206f6620746865206f726967696e616c207461736b2e20546865697220726574727920636f6e66696775726174696f6e2077696c6c20626520646572697665642066726f6d207468654d016f726967696e616c207461736b277320636f6e66696775726174696f6e2c206275742077696c6c20686176652061206c6f7765722076616c756520666f72206072656d61696e696e6760207468616e20746865646f726967696e616c2060746f74616c5f72657472696573602e3063616e63656c5f72657472790401107461736be401785461736b416464726573733c426c6f636b4e756d626572466f723c543e3e000804a852656d6f7665732074686520726574727920636f6e66696775726174696f6e206f662061207461736b2e4863616e63656c5f72657472795f6e616d656404010869640401205461736b4e616d65000904bc43616e63656c2074686520726574727920636f6e66696775726174696f6e206f662061206e616d6564207461736b2e040d01436f6e7461696e7320612076617269616e742070657220646973706174636861626c652065787472696e736963207468617420746869732070616c6c6574206861732e510304184f7074696f6e04045401e40108104e6f6e6500000010536f6d650400e4000001000055030c3070616c6c65745f70726f78791870616c6c65741043616c6c0404540001281470726f78790c01107265616c550201504163636f756e7449644c6f6f6b75704f663c543e000140666f7263655f70726f78795f74797065590301504f7074696f6e3c543a3a50726f7879547970653e00011063616c6c1503017c426f783c3c5420617320436f6e6669673e3a3a52756e74696d6543616c6c3e0000244d0144697370617463682074686520676976656e206063616c6c602066726f6d20616e206163636f756e742074686174207468652073656e64657220697320617574686f726973656420666f72207468726f75676830606164645f70726f7879602e00cc546865206469737061746368206f726967696e20666f7220746869732063616c6c206d757374206265205f5369676e65645f2e002c506172616d65746572733a0d012d20607265616c603a20546865206163636f756e742074686174207468652070726f78792077696c6c206d616b6520612063616c6c206f6e20626568616c66206f662e61012d2060666f7263655f70726f78795f74797065603a2053706563696679207468652065786163742070726f7879207479706520746f206265207573656420616e6420636865636b656420666f7220746869732063616c6c2ed02d206063616c6c603a205468652063616c6c20746f206265206d6164652062792074686520607265616c60206163636f756e742e246164645f70726f78790c012064656c6567617465550201504163636f756e7449644c6f6f6b75704f663c543e00012870726f78795f74797065f00130543a3a50726f78795479706500011464656c6179100144426c6f636b4e756d626572466f723c543e0001244501526567697374657220612070726f7879206163636f756e7420666f72207468652073656e64657220746861742069732061626c6520746f206d616b652063616c6c73206f6e2069747320626568616c662e00cc546865206469737061746368206f726967696e20666f7220746869732063616c6c206d757374206265205f5369676e65645f2e002c506172616d65746572733a11012d206070726f7879603a20546865206163636f756e74207468617420746865206063616c6c65726020776f756c64206c696b6520746f206d616b6520612070726f78792efc2d206070726f78795f74797065603a20546865207065726d697373696f6e7320616c6c6f77656420666f7220746869732070726f7879206163636f756e742e4d012d206064656c6179603a2054686520616e6e6f756e63656d656e7420706572696f64207265717569726564206f662074686520696e697469616c2070726f78792e2057696c6c2067656e6572616c6c79206265147a65726f2e3072656d6f76655f70726f78790c012064656c6567617465550201504163636f756e7449644c6f6f6b75704f663c543e00012870726f78795f74797065f00130543a3a50726f78795479706500011464656c6179100144426c6f636b4e756d626572466f723c543e00021ca8556e726567697374657220612070726f7879206163636f756e7420666f72207468652073656e6465722e00cc546865206469737061746368206f726967696e20666f7220746869732063616c6c206d757374206265205f5369676e65645f2e002c506172616d65746572733a25012d206070726f7879603a20546865206163636f756e74207468617420746865206063616c6c65726020776f756c64206c696b6520746f2072656d6f766520617320612070726f78792e41012d206070726f78795f74797065603a20546865207065726d697373696f6e732063757272656e746c7920656e61626c656420666f72207468652072656d6f7665642070726f7879206163636f756e742e3872656d6f76655f70726f78696573000318b4556e726567697374657220616c6c2070726f7879206163636f756e747320666f72207468652073656e6465722e00cc546865206469737061746368206f726967696e20666f7220746869732063616c6c206d757374206265205f5369676e65645f2e0041015741524e494e473a2054686973206d61792062652063616c6c6564206f6e206163636f756e74732063726561746564206279206070757265602c20686f776576657220696620646f6e652c207468656e590174686520756e726573657276656420666565732077696c6c20626520696e61636365737369626c652e202a2a416c6c2061636365737320746f2074686973206163636f756e742077696c6c206265206c6f73742e2a2a2c6372656174655f707572650c012870726f78795f74797065f00130543a3a50726f78795479706500011464656c6179100144426c6f636b4e756d626572466f723c543e000114696e6465789c010c7531360004483901537061776e2061206672657368206e6577206163636f756e7420746861742069732067756172616e7465656420746f206265206f746865727769736520696e61636365737369626c652c20616e64fc696e697469616c697a65206974207769746820612070726f7879206f66206070726f78795f747970656020666f7220606f726967696e602073656e6465722e006c5265717569726573206120605369676e656460206f726967696e2e0051012d206070726f78795f74797065603a205468652074797065206f66207468652070726f78792074686174207468652073656e6465722077696c6c2062652072656769737465726564206173206f766572207468654d016e6577206163636f756e742e20546869732077696c6c20616c6d6f737420616c7761797320626520746865206d6f7374207065726d697373697665206050726f7879547970656020706f737369626c6520746f78616c6c6f7720666f72206d6178696d756d20666c65786962696c6974792e51012d2060696e646578603a204120646973616d626967756174696f6e20696e6465782c20696e206361736520746869732069732063616c6c6564206d756c7469706c652074696d657320696e207468652073616d655d017472616e73616374696f6e2028652e672e207769746820607574696c6974793a3a626174636860292e20556e6c65737320796f75277265207573696e67206062617463686020796f752070726f6261626c79206a7573744077616e7420746f20757365206030602e4d012d206064656c6179603a2054686520616e6e6f756e63656d656e7420706572696f64207265717569726564206f662074686520696e697469616c2070726f78792e2057696c6c2067656e6572616c6c79206265147a65726f2e0051014661696c73207769746820604475706c69636174656020696620746869732068617320616c7265616479206265656e2063616c6c656420696e2074686973207472616e73616374696f6e2c2066726f6d207468659873616d652073656e6465722c2077697468207468652073616d6520706172616d65746572732e00e44661696c732069662074686572652061726520696e73756666696369656e742066756e647320746f2070617920666f72206465706f7369742e246b696c6c5f7075726514011c737061776e6572550201504163636f756e7449644c6f6f6b75704f663c543e00012870726f78795f74797065f00130543a3a50726f787954797065000114696e6465789c010c75313600011868656967687461010144426c6f636b4e756d626572466f723c543e0001246578745f696e6465786101010c753332000540a052656d6f76657320612070726576696f75736c7920737061776e656420707572652070726f78792e0049015741524e494e473a202a2a416c6c2061636365737320746f2074686973206163636f756e742077696c6c206265206c6f73742e2a2a20416e792066756e64732068656c6420696e2069742077696c6c20626534696e61636365737369626c652e0059015265717569726573206120605369676e656460206f726967696e2c20616e64207468652073656e646572206163636f756e74206d7573742068617665206265656e206372656174656420627920612063616c6c20746f94607075726560207769746820636f72726573706f6e64696e6720706172616d65746572732e0039012d2060737061776e6572603a20546865206163636f756e742074686174206f726967696e616c6c792063616c6c65642060707572656020746f206372656174652074686973206163636f756e742e39012d2060696e646578603a2054686520646973616d626967756174696f6e20696e646578206f726967696e616c6c792070617373656420746f206070757265602e2050726f6261626c79206030602eec2d206070726f78795f74797065603a205468652070726f78792074797065206f726967696e616c6c792070617373656420746f206070757265602e29012d2060686569676874603a2054686520686569676874206f662074686520636861696e207768656e207468652063616c6c20746f20607075726560207761732070726f6365737365642e35012d20606578745f696e646578603a205468652065787472696e73696320696e64657820696e207768696368207468652063616c6c20746f20607075726560207761732070726f6365737365642e0035014661696c73207769746820604e6f5065726d697373696f6e6020696e2063617365207468652063616c6c6572206973206e6f7420612070726576696f75736c7920637265617465642070757265dc6163636f756e742077686f7365206070757265602063616c6c2068617320636f72726573706f6e64696e6720706172616d65746572732e20616e6e6f756e63650801107265616c550201504163636f756e7449644c6f6f6b75704f663c543e00012463616c6c5f6861736834013443616c6c486173684f663c543e00063c05015075626c697368207468652068617368206f6620612070726f78792d63616c6c20746861742077696c6c206265206d61646520696e20746865206675747572652e005d0154686973206d7573742062652063616c6c656420736f6d65206e756d626572206f6620626c6f636b73206265666f72652074686520636f72726573706f6e64696e67206070726f78796020697320617474656d7074656425016966207468652064656c6179206173736f6369617465642077697468207468652070726f78792072656c6174696f6e736869702069732067726561746572207468616e207a65726f2e0011014e6f206d6f7265207468616e20604d617850656e64696e676020616e6e6f756e63656d656e7473206d6179206265206d61646520617420616e79206f6e652074696d652e000901546869732077696c6c2074616b652061206465706f736974206f662060416e6e6f756e63656d656e744465706f736974466163746f72602061732077656c6c206173190160416e6e6f756e63656d656e744465706f736974426173656020696620746865726520617265206e6f206f746865722070656e64696e6720616e6e6f756e63656d656e74732e002501546865206469737061746368206f726967696e20666f7220746869732063616c6c206d757374206265205f5369676e65645f20616e6420612070726f7879206f6620607265616c602e002c506172616d65746572733a0d012d20607265616c603a20546865206163636f756e742074686174207468652070726f78792077696c6c206d616b6520612063616c6c206f6e20626568616c66206f662e15012d206063616c6c5f68617368603a205468652068617368206f66207468652063616c6c20746f206265206d6164652062792074686520607265616c60206163636f756e742e4c72656d6f76655f616e6e6f756e63656d656e740801107265616c550201504163636f756e7449644c6f6f6b75704f663c543e00012463616c6c5f6861736834013443616c6c486173684f663c543e0007287052656d6f7665206120676976656e20616e6e6f756e63656d656e742e0059014d61792062652063616c6c656420627920612070726f7879206163636f756e7420746f2072656d6f766520612063616c6c20746865792070726576696f75736c7920616e6e6f756e63656420616e642072657475726e30746865206465706f7369742e00cc546865206469737061746368206f726967696e20666f7220746869732063616c6c206d757374206265205f5369676e65645f2e002c506172616d65746572733a0d012d20607265616c603a20546865206163636f756e742074686174207468652070726f78792077696c6c206d616b6520612063616c6c206f6e20626568616c66206f662e15012d206063616c6c5f68617368603a205468652068617368206f66207468652063616c6c20746f206265206d6164652062792074686520607265616c60206163636f756e742e4c72656a6563745f616e6e6f756e63656d656e7408012064656c6567617465550201504163636f756e7449644c6f6f6b75704f663c543e00012463616c6c5f6861736834013443616c6c486173684f663c543e000828b052656d6f76652074686520676976656e20616e6e6f756e63656d656e74206f6620612064656c65676174652e0061014d61792062652063616c6c6564206279206120746172676574202870726f7869656429206163636f756e7420746f2072656d6f766520612063616c6c2074686174206f6e65206f662074686569722064656c6567617465732501286064656c656761746560292068617320616e6e6f756e63656420746865792077616e7420746f20657865637574652e20546865206465706f7369742069732072657475726e65642e00cc546865206469737061746368206f726967696e20666f7220746869732063616c6c206d757374206265205f5369676e65645f2e002c506172616d65746572733af42d206064656c6567617465603a20546865206163636f756e7420746861742070726576696f75736c7920616e6e6f756e636564207468652063616c6c2ebc2d206063616c6c5f68617368603a205468652068617368206f66207468652063616c6c20746f206265206d6164652e3c70726f78795f616e6e6f756e63656410012064656c6567617465550201504163636f756e7449644c6f6f6b75704f663c543e0001107265616c550201504163636f756e7449644c6f6f6b75704f663c543e000140666f7263655f70726f78795f74797065590301504f7074696f6e3c543a3a50726f7879547970653e00011063616c6c1503017c426f783c3c5420617320436f6e6669673e3a3a52756e74696d6543616c6c3e00092c4d0144697370617463682074686520676976656e206063616c6c602066726f6d20616e206163636f756e742074686174207468652073656e64657220697320617574686f72697a656420666f72207468726f75676830606164645f70726f7879602e00a852656d6f76657320616e7920636f72726573706f6e64696e6720616e6e6f756e63656d656e742873292e00cc546865206469737061746368206f726967696e20666f7220746869732063616c6c206d757374206265205f5369676e65645f2e002c506172616d65746572733a0d012d20607265616c603a20546865206163636f756e742074686174207468652070726f78792077696c6c206d616b6520612063616c6c206f6e20626568616c66206f662e61012d2060666f7263655f70726f78795f74797065603a2053706563696679207468652065786163742070726f7879207479706520746f206265207573656420616e6420636865636b656420666f7220746869732063616c6c2ed02d206063616c6c603a205468652063616c6c20746f206265206d6164652062792074686520607265616c60206163636f756e742e040d01436f6e7461696e7320612076617269616e742070657220646973706174636861626c652065787472696e736963207468617420746869732070616c6c6574206861732e590304184f7074696f6e04045401f00108104e6f6e6500000010536f6d650400f000000100005d030c3c70616c6c65745f72656769737472791870616c6c65741043616c6c040454000108307365745f6964656e746974790801286964656e746966696564000130543a3a4163636f756e744964000110696e666f610301a4426f783c4964656e74697479496e666f3c543a3a4d61784164646974696f6e616c4669656c64733e3e0000043d01526567697374657220616e206964656e7469747920666f7220616e206163636f756e742e20546869732077696c6c206f766572777269746520616e79206578697374696e67206964656e746974792e38636c6561725f6964656e746974790401286964656e746966696564000130543a3a4163636f756e74496400010484436c65617220746865206964656e74697479206f6620616e206163636f756e742e040d01436f6e7461696e7320612076617269616e742070657220646973706174636861626c652065787472696e736963207468617420746869732070616c6c6574206861732e61030c3c70616c6c65745f7265676973747279147479706573304964656e74697479496e666f04284669656c644c696d697400002401286164646974696f6e616c65030190426f756e6465645665633c28446174612c2044617461292c204669656c644c696d69743e00011c646973706c61796d030110446174610001146c6567616c6d0301104461746100010c7765626d0301104461746100011072696f746d03011044617461000114656d61696c6d0301104461746100013c7067705f66696e6765727072696e74650401404f7074696f6e3c5b75383b2032305d3e000114696d6167656d0301104461746100011c747769747465726d03011044617461000065030c4c626f756e6465645f636f6c6c656374696f6e732c626f756e6465645f76656328426f756e646564566563080454016903045300000400610401185665633c543e00006903000004086d036d03006d030c3c70616c6c65745f7265676973747279147479706573104461746100011901104e6f6e65000000105261773004007103000001001052617731040075030000020010526177320400790300000300105261773304007d030000040010526177340400480000050010526177350400810300000600105261773604008503000007001052617737040089030000080010526177380400a50100000900105261773904008d0300000a001452617731300400910300000b001452617731310400950300000c001452617731320400990300000d0014526177313304009d0300000e001452617731340400a10300000f001452617731350400a503000010001452617731360400a903000011001452617731370400ad03000012001452617731380400b103000013001452617731390400b5030000140014526177323004001101000015001452617732310400b903000016001452617732320400bd03000017001452617732330400c103000018001452617732340400c503000019001452617732350400c90300001a001452617732360400cd0300001b001452617732370400d10300001c001452617732380400d50300001d001452617732390400d90300001e001452617733300400dd0300001f001452617733310400e10300002000145261773332040004000021001452617733330400e503000022001452617733340400e903000023001452617733350400ed03000024001452617733360400f103000025001452617733370400f503000026001452617733380400f903000027001452617733390400fd030000280014526177343004000104000029001452617734310400050400002a001452617734320400090400002b0014526177343304000d0400002c001452617734340400110400002d001452617734350400150400002e001452617734360400190400002f0014526177343704001d040000300014526177343804002104000031001452617734390400250400003200145261773530040029040000330014526177353104002d040000340014526177353204003104000035001452617735330400350400003600145261773534040039040000370014526177353504003d040000380014526177353604004104000039001452617735370400450400003a001452617735380400490400003b0014526177353904004d0400003c001452617736300400510400003d001452617736310400550400003e001452617736320400590400003f0014526177363304005d04000040001452617736340400ed01000041002c426c616b6554776f323536040004000042001853686132353604000400004300244b656363616b323536040004000044002c536861546872656532353604000400004500007103000003000000000800750300000301000000080079030000030200000008007d030000030300000008008103000003050000000800850300000306000000080089030000030700000008008d0300000309000000080091030000030a000000080095030000030b000000080099030000030c00000008009d030000030d0000000800a1030000030e0000000800a5030000030f0000000800a903000003100000000800ad03000003110000000800b103000003120000000800b503000003130000000800b903000003150000000800bd03000003160000000800c103000003170000000800c503000003180000000800c903000003190000000800cd030000031a0000000800d1030000031b0000000800d5030000031c0000000800d9030000031d0000000800dd030000031e0000000800e1030000031f0000000800e503000003210000000800e903000003220000000800ed03000003230000000800f103000003240000000800f503000003250000000800f903000003260000000800fd030000032700000008000104000003280000000800050400000329000000080009040000032a00000008000d040000032b000000080011040000032c000000080015040000032d000000080019040000032e00000008001d040000032f00000008002104000003300000000800250400000331000000080029040000033200000008002d040000033300000008003104000003340000000800350400000335000000080039040000033600000008003d040000033700000008004104000003380000000800450400000339000000080049040000033a00000008004d040000033b000000080051040000033c000000080055040000033d000000080059040000033e00000008005d040000033f00000008006104000002690300650404184f7074696f6e0404540111010108104e6f6e6500000010536f6d6504001101000001000069040c4870616c6c65745f636f6d6d69746d656e74731870616c6c65741043616c6c040454000108387365745f636f6d6d69746d656e740801186e65747569649c010c753136000110696e666f6d040184426f783c436f6d6d69746d656e74496e666f3c543a3a4d61784669656c64733e3e000004945365742074686520636f6d6d69746d656e7420666f72206120676976656e206e6574756964387365745f726174655f6c696d6974040144726174655f6c696d69745f626c6f636b7310010c753332000104885375646f2d7365742074686520636f6d6d69746d656e742072617465206c696d6974040d01436f6e7461696e7320612076617269616e742070657220646973706174636861626c652065787472696e736963207468617420746869732070616c6c6574206861732e6d040c4870616c6c65745f636f6d6d69746d656e747314747970657338436f6d6d69746d656e74496e666f04284669656c644c696d697400000401186669656c647371040170426f756e6465645665633c446174612c204669656c644c696d69743e000071040c4c626f756e6465645f636f6c6c656374696f6e732c626f756e6465645f76656328426f756e646564566563080454017504045300000400790501185665633c543e000075040c4870616c6c65745f636f6d6d69746d656e7473147479706573104461746100011902104e6f6e65000000105261773004007103000001001052617731040075030000020010526177320400790300000300105261773304007d030000040010526177340400480000050010526177350400810300000600105261773604008503000007001052617737040089030000080010526177380400a50100000900105261773904008d0300000a001452617731300400910300000b001452617731310400950300000c001452617731320400990300000d0014526177313304009d0300000e001452617731340400a10300000f001452617731350400a503000010001452617731360400a903000011001452617731370400ad03000012001452617731380400b103000013001452617731390400b5030000140014526177323004001101000015001452617732310400b903000016001452617732320400bd03000017001452617732330400c103000018001452617732340400c503000019001452617732350400c90300001a001452617732360400cd0300001b001452617732370400d10300001c001452617732380400d50300001d001452617732390400d90300001e001452617733300400dd0300001f001452617733310400e10300002000145261773332040004000021001452617733330400e503000022001452617733340400e903000023001452617733350400ed03000024001452617733360400f103000025001452617733370400f503000026001452617733380400f903000027001452617733390400fd030000280014526177343004000104000029001452617734310400050400002a001452617734320400090400002b0014526177343304000d0400002c001452617734340400110400002d001452617734350400150400002e001452617734360400190400002f0014526177343704001d040000300014526177343804002104000031001452617734390400250400003200145261773530040029040000330014526177353104002d040000340014526177353204003104000035001452617735330400350400003600145261773534040039040000370014526177353504003d040000380014526177353604004104000039001452617735370400450400003a001452617735380400490400003b0014526177353904004d0400003c001452617736300400510400003d001452617736310400550400003e001452617736320400590400003f0014526177363304005d04000040001452617736340400ed0100004100145261773635040079040000420014526177363604007d040000430014526177363704008104000044001452617736380400850400004500145261773639040089040000460014526177373004008d0400004700145261773731040091040000480014526177373204009504000049001452617737330400990400004a0014526177373404009d0400004b001452617737350400a10400004c001452617737360400a50400004d001452617737370400a90400004e001452617737380400ad0400004f001452617737390400b104000050001452617738300400b504000051001452617738310400b904000052001452617738320400bd04000053001452617738330400c104000054001452617738340400c504000055001452617738350400c904000056001452617738360400cd04000057001452617738370400d104000058001452617738380400d504000059001452617738390400d90400005a001452617739300400dd0400005b001452617739310400e10400005c001452617739320400e50400005d001452617739330400e90400005e001452617739340400ed0400005f001452617739350400f104000060001452617739360400f504000061001452617739370400f904000062001452617739380400fd040000630014526177393904000105000064001852617731303004000505000065001852617731303104000905000066001852617731303204000d0500006700185261773130330400110500006800185261773130340400150500006900185261773130350400190500006a001852617731303604001d0500006b00185261773130370400210500006c00185261773130380400250500006d00185261773130390400290500006e001852617731313004002d0500006f001852617731313104003105000070001852617731313204003505000071001852617731313304003905000072001852617731313404003d05000073001852617731313504004105000074001852617731313604004505000075001852617731313704004905000076001852617731313804004d0500007700185261773131390400510500007800185261773132300400550500007900185261773132310400590500007a001852617731323204005d0500007b00185261773132330400610500007c00185261773132340400650500007d00185261773132350400690500007e001852617731323604006d0500007f001852617731323704007105000080001852617731323804007505000081002c426c616b6554776f323536040004000082001853686132353604000400008300244b656363616b323536040004000084002c5368615468726565323536040004000085000079040000034100000008007d040000034200000008008104000003430000000800850400000344000000080089040000034500000008008d040000034600000008009104000003470000000800950400000348000000080099040000034900000008009d040000034a0000000800a1040000034b0000000800a5040000034c0000000800a9040000034d0000000800ad040000034e0000000800b1040000034f0000000800b504000003500000000800b904000003510000000800bd04000003520000000800c104000003530000000800c504000003540000000800c904000003550000000800cd04000003560000000800d104000003570000000800d504000003580000000800d904000003590000000800dd040000035a0000000800e1040000035b0000000800e5040000035c0000000800e9040000035d0000000800ed040000035e0000000800f1040000035f0000000800f504000003600000000800f904000003610000000800fd040000036200000008000105000003630000000800050500000364000000080009050000036500000008000d050000036600000008001105000003670000000800150500000368000000080019050000036900000008001d050000036a000000080021050000036b000000080025050000036c000000080029050000036d00000008002d050000036e000000080031050000036f0000000800350500000370000000080039050000037100000008003d050000037200000008004105000003730000000800450500000374000000080049050000037500000008004d050000037600000008005105000003770000000800550500000378000000080059050000037900000008005d050000037a000000080061050000037b000000080065050000037c000000080069050000037d00000008006d050000037e000000080071050000037f0000000800750500000380000000080079050000027504007d050c4870616c6c65745f61646d696e5f7574696c731870616c6c65741043616c6c0404540001c440737761705f617574686f72697469657304013c6e65775f617574686f726974696573b50101e4426f756e6465645665633c3c5420617320436f6e6669673e3a3a417574686f7269747949642c20543a3a4d6178417574686f7269746965733e00000ce85468652065787472696e736963207365747320746865206e657720617574686f72697469657320666f72204175726120636f6e73656e7375732ea04974206973206f6e6c792063616c6c61626c652062792074686520726f6f74206163636f756e742e09015468652065787472696e7369632077696c6c2063616c6c2074686520417572612070616c6c657420746f206368616e67652074686520617574686f7269746965732e547375646f5f7365745f64656661756c745f74616b6504013064656661756c745f74616b659c010c75313600010cd05468652065787472696e7369632073657473207468652064656661756c742074616b6520666f7220746865206e6574776f726b2ea04974206973206f6e6c792063616c6c61626c652062792074686520726f6f74206163636f756e742e15015468652065787472696e7369632077696c6c2063616c6c207468652053756274656e736f722070616c6c657420746f20736574207468652064656661756c742074616b652e587375646f5f7365745f74785f726174655f6c696d697404013474785f726174655f6c696d697418010c75363400020cf85468652065787472696e736963207365747320746865207472616e73616374696f6e2072617465206c696d697420666f7220746865206e6574776f726b2ea04974206973206f6e6c792063616c6c61626c652062792074686520726f6f74206163636f756e742e3d015468652065787472696e7369632077696c6c2063616c6c207468652053756274656e736f722070616c6c657420746f2073657420746865207472616e73616374696f6e2072617465206c696d69742e6c7375646f5f7365745f73657276696e675f726174655f6c696d69740801186e65747569649c010c75313600014873657276696e675f726174655f6c696d697418010c75363400030cdc5468652065787472696e7369632073657473207468652073657276696e672072617465206c696d697420666f722061207375626e65742ee04974206973206f6e6c792063616c6c61626c652062792074686520726f6f74206163636f756e74206f72207375626e6574206f776e65722e2d015468652065787472696e7369632077696c6c2063616c6c207468652053756274656e736f722070616c6c657420746f20736574207468652073657276696e672072617465206c696d69742e5c7375646f5f7365745f6d696e5f646966666963756c74790801186e65747569649c010c7531360001386d696e5f646966666963756c747918010c75363400040cdc5468652065787472696e736963207365747320746865206d696e696d756d20646966666963756c747920666f722061207375626e65742ee04974206973206f6e6c792063616c6c61626c652062792074686520726f6f74206163636f756e74206f72207375626e6574206f776e65722e2d015468652065787472696e7369632077696c6c2063616c6c207468652053756274656e736f722070616c6c657420746f2073657420746865206d696e696d756d20646966666963756c74792e5c7375646f5f7365745f6d61785f646966666963756c74790801186e65747569649c010c7531360001386d61785f646966666963756c747918010c75363400050cdc5468652065787472696e736963207365747320746865206d6178696d756d20646966666963756c747920666f722061207375626e65742ee04974206973206f6e6c792063616c6c61626c652062792074686520726f6f74206163636f756e74206f72207375626e6574206f776e65722e2d015468652065787472696e7369632077696c6c2063616c6c207468652053756274656e736f722070616c6c657420746f2073657420746865206d6178696d756d20646966666963756c74792e707375646f5f7365745f776569676874735f76657273696f6e5f6b65790801186e65747569649c010c75313600014c776569676874735f76657273696f6e5f6b657918010c75363400060ce05468652065787472696e73696320736574732074686520776569676874732076657273696f6e206b657920666f722061207375626e65742ee04974206973206f6e6c792063616c6c61626c652062792074686520726f6f74206163636f756e74206f72207375626e6574206f776e65722e31015468652065787472696e7369632077696c6c2063616c6c207468652053756274656e736f722070616c6c657420746f207365742074686520776569676874732076657273696f6e206b65792e7c7375646f5f7365745f776569676874735f7365745f726174655f6c696d69740801186e65747569649c010c753136000158776569676874735f7365745f726174655f6c696d697418010c75363400070cec5468652065787472696e7369632073657473207468652077656967687473207365742072617465206c696d697420666f722061207375626e65742ee04974206973206f6e6c792063616c6c61626c652062792074686520726f6f74206163636f756e74206f72207375626e6574206f776e65722e3d015468652065787472696e7369632077696c6c2063616c6c207468652053756274656e736f722070616c6c657420746f20736574207468652077656967687473207365742072617465206c696d69742e707375646f5f7365745f61646a7573746d656e745f696e74657276616c0801186e65747569649c010c75313600014c61646a7573746d656e745f696e74657276616c9c010c75313600080ce05468652065787472696e7369632073657473207468652061646a7573746d656e7420696e74657276616c20666f722061207375626e65742e31014974206973206f6e6c792063616c6c61626c652062792074686520726f6f74206163636f756e742c206e6f74206368616e676561626c6520627920746865207375626e6574206f776e65722e31015468652065787472696e7369632077696c6c2063616c6c207468652053756274656e736f722070616c6c657420746f20736574207468652061646a7573746d656e7420696e74657276616c2e647375646f5f7365745f61646a7573746d656e745f616c7068610801186e65747569649c010c75313600014061646a7573746d656e745f616c70686118010c75363400090cd45468652065787472696e7369632073657473207468652061646a7573746d656e7420616c70686120666f722061207375626e65742ee04974206973206f6e6c792063616c6c61626c652062792074686520726f6f74206163636f756e74206f72207375626e6574206f776e65722e25015468652065787472696e7369632077696c6c2063616c6c207468652053756274656e736f722070616c6c657420746f20736574207468652061646a7573746d656e7420616c7068612e647375646f5f7365745f6d61785f7765696768745f6c696d69740801186e65747569649c010c7531360001406d61785f7765696768745f6c696d69749c010c753136000c0cd05468652065787472696e7369632073657473207468652061646a7573746d656e74206265746120666f722061207375626e65742ee04974206973206f6e6c792063616c6c61626c652062792074686520726f6f74206163636f756e74206f72207375626e6574206f776e65722e21015468652065787472696e7369632077696c6c2063616c6c207468652053756274656e736f722070616c6c657420746f20736574207468652061646a7573746d656e7420626574612e607375646f5f7365745f696d6d756e6974795f706572696f640801186e65747569649c010c75313600013c696d6d756e6974795f706572696f649c010c753136000d0cd05468652065787472696e73696320736574732074686520696d6d756e69747920706572696f6420666f722061207375626e65742ee04974206973206f6e6c792063616c6c61626c652062792074686520726f6f74206163636f756e74206f72207375626e6574206f776e65722e21015468652065787472696e7369632077696c6c2063616c6c207468652053756274656e736f722070616c6c657420746f207365742074686520696d6d756e69747920706572696f642e707375646f5f7365745f6d696e5f616c6c6f7765645f776569676874730801186e65747569649c010c75313600014c6d696e5f616c6c6f7765645f776569676874739c010c753136000e0cf05468652065787472696e736963207365747320746865206d696e696d756d20616c6c6f776564207765696768747320666f722061207375626e65742ee04974206973206f6e6c792063616c6c61626c652062792074686520726f6f74206163636f756e74206f72207375626e6574206f776e65722e41015468652065787472696e7369632077696c6c2063616c6c207468652053756274656e736f722070616c6c657420746f2073657420746865206d696e696d756d20616c6c6f77656420776569676874732e647375646f5f7365745f6d61785f616c6c6f7765645f756964730801186e65747569649c010c7531360001406d61785f616c6c6f7765645f756964739c010c753136000f0ce45468652065787472696e736963207365747320746865206d6178696d756d20616c6c6f776564205549447320666f722061207375626e65742ea04974206973206f6e6c792063616c6c61626c652062792074686520726f6f74206163636f756e742e69015468652065787472696e7369632077696c6c2063616c6c207468652053756274656e736f722070616c6c657420746f2073657420746865206d6178696d756d20616c6c6f776564205549447320666f722061207375626e65742e387375646f5f7365745f6b617070610801186e65747569649c010c7531360001146b617070619c010c75313600100ca85468652065787472696e736963207365747320746865206b6170706120666f722061207375626e65742ee04974206973206f6e6c792063616c6c61626c652062792074686520726f6f74206163636f756e74206f72207375626e6574206f776e65722ef85468652065787472696e7369632077696c6c2063616c6c207468652053756274656e736f722070616c6c657420746f2073657420746865206b617070612e307375646f5f7365745f72686f0801186e65747569649c010c75313600010c72686f9c010c75313600110ca05468652065787472696e7369632073657473207468652072686f20666f722061207375626e65742ee04974206973206f6e6c792063616c6c61626c652062792074686520726f6f74206163636f756e74206f72207375626e6574206f776e65722ef05468652065787472696e7369632077696c6c2063616c6c207468652053756274656e736f722070616c6c657420746f20736574207468652072686f2e607375646f5f7365745f61637469766974795f6375746f66660801186e65747569649c010c75313600013c61637469766974795f6375746f66669c010c75313600120cd05468652065787472696e736963207365747320746865206163746976697479206375746f666620666f722061207375626e65742ee04974206973206f6e6c792063616c6c61626c652062792074686520726f6f74206163636f756e74206f72207375626e6574206f776e65722e21015468652065787472696e7369632077696c6c2063616c6c207468652053756274656e736f722070616c6c657420746f2073657420746865206163746976697479206375746f66662e947375646f5f7365745f6e6574776f726b5f726567697374726174696f6e5f616c6c6f7765640801186e65747569649c010c753136000150726567697374726174696f6e5f616c6c6f776564240110626f6f6c00130c05015468652065787472696e736963207365747320746865206e6574776f726b20726567697374726174696f6e20616c6c6f77656420666f722061207375626e65742ee04974206973206f6e6c792063616c6c61626c652062792074686520726f6f74206163636f756e74206f72207375626e6574206f776e65722e55015468652065787472696e7369632077696c6c2063616c6c207468652053756274656e736f722070616c6c657420746f2073657420746865206e6574776f726b20726567697374726174696f6e20616c6c6f7765642ea47375646f5f7365745f6e6574776f726b5f706f775f726567697374726174696f6e5f616c6c6f7765640801186e65747569649c010c753136000150726567697374726174696f6e5f616c6c6f776564240110626f6f6c00140c15015468652065787472696e736963207365747320746865206e6574776f726b20506f5720726567697374726174696f6e20616c6c6f77656420666f722061207375626e65742ee04974206973206f6e6c792063616c6c61626c652062792074686520726f6f74206163636f756e74206f72207375626e6574206f776e65722e65015468652065787472696e7369632077696c6c2063616c6c207468652053756274656e736f722070616c6c657420746f2073657420746865206e6574776f726b20506f5720726567697374726174696f6e20616c6c6f7765642ea87375646f5f7365745f7461726765745f726567697374726174696f6e735f7065725f696e74657276616c0801186e65747569649c010c7531360001847461726765745f726567697374726174696f6e735f7065725f696e74657276616c9c010c75313600150c19015468652065787472696e7369632073657473207468652074617267657420726567697374726174696f6e732070657220696e74657276616c20666f722061207375626e65742ea04974206973206f6e6c792063616c6c61626c652062792074686520726f6f74206163636f756e742e69015468652065787472696e7369632077696c6c2063616c6c207468652053756274656e736f722070616c6c657420746f20736574207468652074617267657420726567697374726174696f6e732070657220696e74657276616c2e447375646f5f7365745f6d696e5f6275726e0801186e65747569649c010c7531360001206d696e5f6275726e18010c75363400160cc45468652065787472696e736963207365747320746865206d696e696d756d206275726e20666f722061207375626e65742ee04974206973206f6e6c792063616c6c61626c652062792074686520726f6f74206163636f756e74206f72207375626e6574206f776e65722e15015468652065787472696e7369632077696c6c2063616c6c207468652053756274656e736f722070616c6c657420746f2073657420746865206d696e696d756d206275726e2e447375646f5f7365745f6d61785f6275726e0801186e65747569649c010c7531360001206d61785f6275726e18010c75363400170cc45468652065787472696e736963207365747320746865206d6178696d756d206275726e20666f722061207375626e65742ee04974206973206f6e6c792063616c6c61626c652062792074686520726f6f74206163636f756e74206f72207375626e6574206f776e65722e15015468652065787472696e7369632077696c6c2063616c6c207468652053756274656e736f722070616c6c657420746f2073657420746865206d6178696d756d206275726e2e4c7375646f5f7365745f646966666963756c74790801186e65747569649c010c753136000128646966666963756c747918010c75363400180cbc5468652065787472696e73696320736574732074686520646966666963756c747920666f722061207375626e65742ee04974206973206f6e6c792063616c6c61626c652062792074686520726f6f74206163636f756e74206f72207375626e6574206f776e65722e0d015468652065787472696e7369632077696c6c2063616c6c207468652053756274656e736f722070616c6c657420746f207365742074686520646966666963756c74792e7c7375646f5f7365745f6d61785f616c6c6f7765645f76616c696461746f72730801186e65747569649c010c7531360001586d61785f616c6c6f7765645f76616c696461746f72739c010c75313600190cfc5468652065787472696e736963207365747320746865206d6178696d756d20616c6c6f7765642076616c696461746f727320666f722061207375626e65742ea04974206973206f6e6c792063616c6c61626c652062792074686520726f6f74206163636f756e742e4d015468652065787472696e7369632077696c6c2063616c6c207468652053756274656e736f722070616c6c657420746f2073657420746865206d6178696d756d20616c6c6f7765642076616c696461746f72732e747375646f5f7365745f626f6e64735f6d6f76696e675f617665726167650801186e65747569649c010c753136000150626f6e64735f6d6f76696e675f6176657261676518010c753634001a0ce45468652065787472696e73696320736574732074686520626f6e6473206d6f76696e67206176657261676520666f722061207375626e65742ee04974206973206f6e6c792063616c6c61626c652062792074686520726f6f74206163636f756e74206f72207375626e6574206f776e65722e35015468652065787472696e7369632077696c6c2063616c6c207468652053756274656e736f722070616c6c657420746f207365742074686520626f6e6473206d6f76696e6720617665726167652e587375646f5f7365745f626f6e64735f70656e616c74790801186e65747569649c010c753136000134626f6e64735f70656e616c74799c010c753136003c0cc85468652065787472696e73696320736574732074686520626f6e64732070656e616c747920666f722061207375626e65742ee04974206973206f6e6c792063616c6c61626c652062792074686520726f6f74206163636f756e74206f72207375626e6574206f776e65722e19015468652065787472696e7369632077696c6c2063616c6c207468652053756274656e736f722070616c6c657420746f207365742074686520626f6e64732070656e616c74792e907375646f5f7365745f6d61785f726567697374726174696f6e735f7065725f626c6f636b0801186e65747569649c010c75313600016c6d61785f726567697374726174696f6e735f7065725f626c6f636b9c010c753136001b0c11015468652065787472696e736963207365747320746865206d6178696d756d20726567697374726174696f6e732070657220626c6f636b20666f722061207375626e65742ea04974206973206f6e6c792063616c6c61626c652062792074686520726f6f74206163636f756e742e61015468652065787472696e7369632077696c6c2063616c6c207468652053756274656e736f722070616c6c657420746f2073657420746865206d6178696d756d20726567697374726174696f6e732070657220626c6f636b2e647375646f5f7365745f7375626e65745f6f776e65725f6375740401407375626e65745f6f776e65725f6375749c010c753136001c0cd45468652065787472696e736963207365747320746865207375626e6574206f776e65722063757420666f722061207375626e65742ea04974206973206f6e6c792063616c6c61626c652062792074686520726f6f74206163636f756e742e25015468652065787472696e7369632077696c6c2063616c6c207468652053756274656e736f722070616c6c657420746f2073657420746865207375626e6574206f776e6572206375742e6c7375646f5f7365745f6e6574776f726b5f726174655f6c696d6974040128726174655f6c696d697418010c753634001d0ce85468652065787472696e736963207365747320746865206e6574776f726b2072617465206c696d697420666f7220746865206e6574776f726b2ea04974206973206f6e6c792063616c6c61626c652062792074686520726f6f74206163636f756e742e2d015468652065787472696e7369632077696c6c2063616c6c207468652053756274656e736f722070616c6c657420746f2073657420746865206e6574776f726b2072617465206c696d69742e387375646f5f7365745f74656d706f0801186e65747569649c010c75313600011474656d706f9c010c753136001e0ca85468652065787472696e7369632073657473207468652074656d706f20666f722061207375626e65742ea04974206973206f6e6c792063616c6c61626c652062792074686520726f6f74206163636f756e742ef85468652065787472696e7369632077696c6c2063616c6c207468652053756274656e736f722070616c6c657420746f20736574207468652074656d706f2e5c7375646f5f7365745f746f74616c5f69737375616e6365040138746f74616c5f69737375616e636518010c75363400210cd85468652065787472696e73696320736574732074686520746f74616c2069737375616e636520666f7220746865206e6574776f726b2ea04974206973206f6e6c792063616c6c61626c652062792074686520726f6f74206163636f756e742e45015468652065787472696e7369632077696c6c2063616c6c207468652053756274656e736f722070616c6c657420746f20736574207468652069737375616e636520666f7220746865206e6574776f726b2e807375646f5f7365745f6e6574776f726b5f696d6d756e6974795f706572696f6404013c696d6d756e6974795f706572696f6418010c75363400230cdc5468652065787472696e73696320736574732074686520696d6d756e69747920706572696f6420666f7220746865206e6574776f726b2ea04974206973206f6e6c792063616c6c61626c652062792074686520726f6f74206163636f756e742e61015468652065787472696e7369632077696c6c2063616c6c207468652053756274656e736f722070616c6c657420746f207365742074686520696d6d756e69747920706572696f6420666f7220746865206e6574776f726b2e787375646f5f7365745f6e6574776f726b5f6d696e5f6c6f636b5f636f73740401246c6f636b5f636f737418010c75363400240cd45468652065787472696e736963207365747320746865206d696e206c6f636b20636f737420666f7220746865206e6574776f726b2ea04974206973206f6e6c792063616c6c61626c652062792074686520726f6f74206163636f756e742e59015468652065787472696e7369632077696c6c2063616c6c207468652053756274656e736f722070616c6c657420746f2073657420746865206d696e206c6f636b20636f737420666f7220746865206e6574776f726b2e547375646f5f7365745f7375626e65745f6c696d697404012c6d61785f7375626e6574739c010c75313600250cd05468652065787472696e736963207365747320746865207375626e6574206c696d697420666f7220746865206e6574776f726b2ea04974206973206f6e6c792063616c6c61626c652062792074686520726f6f74206163636f756e742e15015468652065787472696e7369632077696c6c2063616c6c207468652053756274656e736f722070616c6c657420746f2073657420746865207375626e6574206c696d69742e807375646f5f7365745f6c6f636b5f726564756374696f6e5f696e74657276616c040120696e74657276616c18010c75363400260cfc5468652065787472696e736963207365747320746865206c6f636b20726564756374696f6e20696e74657276616c20666f7220746865206e6574776f726b2ea04974206973206f6e6c792063616c6c61626c652062792074686520726f6f74206163636f756e742e41015468652065787472696e7369632077696c6c2063616c6c207468652053756274656e736f722070616c6c657420746f2073657420746865206c6f636b20726564756374696f6e20696e74657276616c2e547375646f5f7365745f72616f5f72656379636c65640801186e65747569649c010c75313600013072616f5f72656379636c656418010c75363400270cc45468652065787472696e7369632073657473207468652072656379636c65642052414f20666f722061207375626e65742ea04974206973206f6e6c792063616c6c61626c652062792074686520726f6f74206163636f756e742e15015468652065787472696e7369632077696c6c2063616c6c207468652053756274656e736f722070616c6c657420746f20736574207468652072656379636c65642052414f2e607375646f5f7365745f7374616b655f7468726573686f6c640401246d696e5f7374616b6518010c753634002a0ca45468652065787472696e7369632073657473207468652077656967687473206d696e207374616b652ea04974206973206f6e6c792063616c6c61626c652062792074686520726f6f74206163636f756e742e29015468652065787472696e7369632077696c6c2063616c6c207468652053756274656e736f722070616c6c657420746f20736574207468652077656967687473206d696e207374616b652e947375646f5f7365745f6e6f6d696e61746f725f6d696e5f72657175697265645f7374616b650401246d696e5f7374616b6518010c753634002b0cf45468652065787472696e736963207365747320746865206d696e696d756d207374616b6520726571756972656420666f72206e6f6d696e61746f72732ea04974206973206f6e6c792063616c6c61626c652062792074686520726f6f74206163636f756e742e79015468652065787472696e7369632077696c6c2063616c6c207468652053756274656e736f722070616c6c657420746f2073657420746865206d696e696d756d207374616b6520726571756972656420666f72206e6f6d696e61746f72732e907375646f5f7365745f74785f64656c65676174655f74616b655f726174655f6c696d697404013474785f726174655f6c696d697418010c753634002d0c05015468652065787472696e7369632073657473207468652072617465206c696d697420666f722064656c65676174652074616b65207472616e73616374696f6e732ea04974206973206f6e6c792063616c6c61626c652062792074686520726f6f74206163636f756e742e89015468652065787472696e7369632077696c6c2063616c6c207468652053756274656e736f722070616c6c657420746f20736574207468652072617465206c696d697420666f722064656c65676174652074616b65207472616e73616374696f6e732e687375646f5f7365745f6d696e5f64656c65676174655f74616b6504011074616b659c010c753136002e0cb45468652065787472696e736963207365747320746865206d696e696d756d2064656c65676174652074616b652ea04974206973206f6e6c792063616c6c61626c652062792074686520726f6f74206163636f756e742e39015468652065787472696e7369632077696c6c2063616c6c207468652053756274656e736f722070616c6c657420746f2073657420746865206d696e696d756d2064656c65676174652074616b652e987375646f5f7365745f636f6d6d69745f72657665616c5f776569676874735f656e61626c65640801186e65747569649c010c75313600011c656e61626c6564240110626f6f6c00310c05015468652065787472696e73696320656e61626c65642f64697361626c657320636f6d6d69742f7265617665616c20666f72206120676976656e207375626e65742ee04974206973206f6e6c792063616c6c61626c652062792074686520726f6f74206163636f756e74206f72207375626e6574206f776e65722ef85468652065787472696e7369632077696c6c2063616c6c207468652053756274656e736f722070616c6c657420746f20736574207468652076616c75652e747375646f5f7365745f6c69717569645f616c7068615f656e61626c65640801186e65747569649c010c75313600011c656e61626c6564240110626f6f6c003224d0456e61626c6573206f722064697361626c6573204c697175696420416c70686120666f72206120676976656e207375626e65742e00302320506172616d65746572734d012d20606f726967696e603a20546865206f726967696e206f66207468652063616c6c2c207768696368206d7573742062652074686520726f6f74206163636f756e74206f72207375626e6574206f776e65722ec42d20606e6574756964603a2054686520756e69717565206964656e74696669657220666f7220746865207375626e65742ef82d2060656e61626c6564603a204120626f6f6c65616e20666c616720746f20656e61626c65206f722064697361626c65204c697175696420416c7068612e00202320576569676874cd01546869732066756e6374696f6e20686173206120666978656420776569676874206f66203020616e6420697320636c617373696669656420617320616e206f7065726174696f6e616c207472616e73616374696f6e207468617420646f6573206e6f7420696e63757220616e7920666565732e547375646f5f7365745f616c7068615f76616c7565730c01186e65747569649c010c753136000124616c7068615f6c6f779c010c753136000128616c7068615f686967689c010c75313600330470536574732076616c75657320666f72206c697175696420616c706861687375646f5f7365745f6e6574776f726b5f6d61785f7374616b650801186e65747569649c010c7531360001246d61785f7374616b6518010c753634003564d85365747320746865206d6178696d756d207374616b6520616c6c6f77656420666f722061207370656369666963206e6574776f726b2e004d01546869732066756e6374696f6e20616c6c6f77732074686520726f6f74206163636f756e7420746f2073657420746865206d6178696d756d207374616b6520666f72206120676976656e206e6574776f726b2e05014974207570646174657320746865206e6574776f726b2773206d6178696d756d207374616b652076616c756520616e64206c6f677320746865206368616e67652e002c2320417267756d656e74730011012a20606f726967696e60202d20546865206f726967696e206f66207468652063616c6c2c207768696368206d7573742062652074686520726f6f74206163636f756e742ec82a20606e657475696460202d2054686520756e69717565206964656e746966696572206f6620746865206e6574776f726b2ecc2a20606d61785f7374616b6560202d20546865206e6577206d6178696d756d207374616b652076616c756520746f207365742e0024232052657475726e7300250152657475726e7320604f6b282829296020696620746865206f7065726174696f6e206973207375636365737366756c2c206f7220616e206572726f72206966206974206661696c732e002423204578616d706c6500001c23204e6f74657300dc2d20546869732066756e6374696f6e2063616e206f6e6c792062652063616c6c65642062792074686520726f6f74206163636f756e742ee02d2054686520606e6574756964602073686f756c6420636f72726573706f6e6420746f20616e206578697374696e67206e6574776f726b2e00182320544f444f009c7375646f5f7365745f636f6c646b65795f737761705f7363686564756c655f6475726174696f6e0401206475726174696f6e100144426c6f636b4e756d626572466f723c543e003638bc5365747320746865206475726174696f6e206f662074686520636f6c646b65792073776170207363686564756c652e006501546869732065787472696e73696320616c6c6f77732074686520726f6f74206163636f756e7420746f2073657420746865206475726174696f6e20666f722074686520636f6c646b65792073776170207363686564756c652e810154686520636f6c646b65792073776170207363686564756c652064657465726d696e657320686f77206c6f6e672069742074616b657320666f72206120636f6c646b65792073776170206f7065726174696f6e20746f20636f6d706c6574652e002c2320417267756d656e747311012a20606f726967696e60202d20546865206f726967696e206f66207468652063616c6c2c207768696368206d7573742062652074686520726f6f74206163636f756e742e4d012a20606475726174696f6e60202d20546865206e6577206475726174696f6e20666f722074686520636f6c646b65792073776170207363686564756c652c20696e206e756d626572206f6620626c6f636b732e002023204572726f7273d82a20604261644f726967696e60202d204966207468652063616c6c6572206973206e6f742074686520726f6f74206163636f756e742e00202320576569676874dc5765696768742069732068616e646c6564206279207468652060235b70616c6c65743a3a7765696768745d60206174747269627574652eac7375646f5f7365745f646973736f6c76655f6e6574776f726b5f7363686564756c655f6475726174696f6e0401206475726174696f6e100144426c6f636b4e756d626572466f723c543e003738cc5365747320746865206475726174696f6e206f662074686520646973736f6c7665206e6574776f726b207363686564756c652e007501546869732065787472696e73696320616c6c6f77732074686520726f6f74206163636f756e7420746f2073657420746865206475726174696f6e20666f722074686520646973736f6c7665206e6574776f726b207363686564756c652ead0154686520646973736f6c7665206e6574776f726b207363686564756c652064657465726d696e657320686f77206c6f6e672069742074616b657320666f722061206e6574776f726b20646973736f6c7574696f6e206f7065726174696f6e20746f20636f6d706c6574652e002c2320417267756d656e747311012a20606f726967696e60202d20546865206f726967696e206f66207468652063616c6c2c207768696368206d7573742062652074686520726f6f74206163636f756e742e5d012a20606475726174696f6e60202d20546865206e6577206475726174696f6e20666f722074686520646973736f6c7665206e6574776f726b207363686564756c652c20696e206e756d626572206f6620626c6f636b732e002023204572726f7273d82a20604261644f726967696e60202d204966207468652063616c6c6572206973206e6f742074686520726f6f74206163636f756e742e00202320576569676874dc5765696768742069732068616e646c6564206279207468652060235b70616c6c65743a3a7765696768745d60206174747269627574652e9c7375646f5f7365745f636f6d6d69745f72657665616c5f776569676874735f696e74657276616c0801186e65747569649c010c753136000120696e74657276616c18010c753634003940f4536574732074686520636f6d6d69742d72657665616c207765696768747320706572696f647320666f722061207370656369666963207375626e65742e001d02546869732065787472696e73696320616c6c6f777320746865207375626e6574206f776e6572206f7220726f6f74206163636f756e7420746f2073657420746865206475726174696f6e2028696e2065706f6368732920647572696e6720776869636820636f6d6d69747465642077656967687473206d7573742062652072657665616c65642ee10154686520636f6d6d69742d72657665616c206d656368616e69736d20656e7375726573207468617420757365727320636f6d6d6974207765696768747320696e20616476616e636520616e642072657665616c207468656d206f6e6c792077697468696e20612073706563696669656420706572696f642e002c2320417267756d656e747361012a20606f726967696e60202d20546865206f726967696e206f66207468652063616c6c2c207768696368206d75737420626520746865207375626e6574206f776e6572206f722074686520726f6f74206163636f756e742e55012a20606e657475696460202d2054686520756e69717565206964656e746966696572206f6620746865207375626e657420666f722077686963682074686520706572696f647320617265206265696e67207365742e21012a2060706572696f647360202d20546865206e756d626572206f662065706f636873207468617420646566696e652074686520636f6d6d69742d72657665616c20706572696f642e002023204572726f72733d012a20604261644f726967696e60202d204966207468652063616c6c6572206973206e65697468657220746865207375626e6574206f776e6572206e6f722074686520726f6f74206163636f756e742e01012a20605375626e6574446f65734e6f74457869737460202d2049662074686520737065636966696564207375626e657420646f6573206e6f742065786973742e00202320576569676874dc5765696768742069732068616e646c6564206279207468652060235b70616c6c65743a3a7765696768745d60206174747269627574652e547375646f5f7365745f65766d5f636861696e5f6964040120636861696e5f696418010c753634003a2c5453657473207468652045564d20436861696e49442e002c2320417267756d656e747361012a20606f726967696e60202d20546865206f726967696e206f66207468652063616c6c2c207768696368206d75737420626520746865207375626e6574206f776e6572206f722074686520726f6f74206163636f756e742e782a2060636861696e496460202d205468652075363420636861696e204944002023204572726f72733d012a20604261644f726967696e60202d204966207468652063616c6c6572206973206e65697468657220746865207375626e6574206f776e6572206e6f722074686520726f6f74206163636f756e742e00202320576569676874dc5765696768742069732068616e646c6564206279207468652060235b70616c6c65743a3a7765696768745d60206174747269627574652e5c7363686564756c655f6772616e6470615f6368616e67650c01406e6578745f617574686f726974696573800134417574686f726974794c697374000124696e5f626c6f636b73100144426c6f636b4e756d626572466f723c543e000118666f72636564d10101644f7074696f6e3c426c6f636b4e756d626572466f723c543e3e003b3c250141207075626c696320696e7465726661636520666f72206070616c6c65745f6772616e6470613a3a50616c6c65743a3a7363686564756c655f6772616e6470615f6368616e6765602e00945363686564756c652061206368616e676520696e2074686520617574686f7269746965732e005501546865206368616e67652077696c6c206265206170706c6965642061742074686520656e64206f6620657865637574696f6e206f662074686520626c6f636b2060696e5f626c6f636b736020616674657220746865550163757272656e7420626c6f636b2e20546869732076616c7565206d617920626520302c20696e207768696368206361736520746865206368616e6765206973206170706c6965642061742074686520656e64206f66487468652063757272656e7420626c6f636b2e0049014966207468652060666f726365646020706172616d6574657220697320646566696e65642c207468697320696e646963617465732074686174207468652063757272656e742073657420686173206265656e490173796e6368726f6e6f75736c792064657465726d696e656420746f206265206f66666c696e6520616e6420746861742061667465722060696e5f626c6f636b73602074686520676976656e206368616e67654d0173686f756c64206265206170706c6965642e2054686520676976656e20626c6f636b206e756d62657220696e6469636174657320746865206d656469616e206c6173742066696e616c697a656420626c6f636b51016e756d62657220616e642069742073686f756c642062652075736564206173207468652063616e6f6e20626c6f636b207768656e207374617274696e6720746865206e6577206772616e64706120766f7465722e0059014e6f206368616e67652073686f756c64206265207369676e616c6564207768696c6520616e79206368616e67652069732070656e64696e672e2052657475726e7320616e206572726f722069662061206368616e67654c697320616c72656164792070656e64696e672e046501446973706174636861626c652066756e6374696f6e7320616c6c6f777320757365727320746f20696e7465726163742077697468207468652070616c6c657420616e6420696e766f6b65207374617465206368616e6765732e81050c4070616c6c65745f736166655f6d6f64651870616c6c65741043616c6c04045400012014656e7465720000181901456e74657220736166652d6d6f6465207065726d697373696f6e6c6573736c7920666f72205b60436f6e6669673a3a456e7465724475726174696f6e605d20626c6f636b732e0009015265736572766573205b60436f6e6669673a3a456e7465724465706f736974416d6f756e74605d2066726f6d207468652063616c6c65722773206163636f756e742eb4456d69747320616e205b604576656e743a3a456e7465726564605d206576656e74206f6e20737563636573732e0d014572726f72732077697468205b604572726f723a3a456e7465726564605d2069662074686520736166652d6d6f646520697320616c726561647920656e74657265642e15014572726f72732077697468205b604572726f723a3a4e6f74436f6e66696775726564605d20696620746865206465706f73697420616d6f756e7420697320604e6f6e65602e2c666f7263655f656e7465720001181901456e74657220736166652d6d6f646520627920666f72636520666f722061207065722d6f726967696e20636f6e66696775726564206e756d626572206f6620626c6f636b732e00b4456d69747320616e205b604576656e743a3a456e7465726564605d206576656e74206f6e20737563636573732e0d014572726f72732077697468205b604572726f723a3a456e7465726564605d2069662074686520736166652d6d6f646520697320616c726561647920656e74657265642e00f843616e206f6e6c792062652063616c6c656420627920746865205b60436f6e6669673a3a466f726365456e7465724f726967696e605d206f726967696e2e18657874656e6400022c3101457874656e642074686520736166652d6d6f6465207065726d697373696f6e6c6573736c7920666f72205b60436f6e6669673a3a457874656e644475726174696f6e605d20626c6f636b732e00e85468697320616363756d756c61746573206f6e20746f70206f66207468652063757272656e742072656d61696e696e67206475726174696f6e2e0d015265736572766573205b60436f6e6669673a3a457874656e644465706f736974416d6f756e74605d2066726f6d207468652063616c6c65722773206163636f756e742eb8456d69747320616e205b604576656e743a3a457874656e646564605d206576656e74206f6e20737563636573732ee84572726f72732077697468205b604572726f723a3a457869746564605d2069662074686520736166652d6d6f646520697320656e74657265642e15014572726f72732077697468205b604572726f723a3a4e6f74436f6e66696775726564605d20696620746865206465706f73697420616d6f756e7420697320604e6f6e65602e00450154686973206d61792062652063616c6c656420627920616e79207369676e6564206f726967696e2077697468205b60436f6e6669673a3a457874656e644465706f736974416d6f756e74605d2066726565350163757272656e637920746f20726573657276652e20546869732063616c6c2063616e2062652064697361626c656420666f7220616c6c206f726967696e7320627920636f6e6669677572696e67a85b60436f6e6669673a3a457874656e644465706f736974416d6f756e74605d20746f20604e6f6e65602e30666f7263655f657874656e640003182d01457874656e642074686520736166652d6d6f646520627920666f72636520666f722061207065722d6f726967696e20636f6e66696775726564206e756d626572206f6620626c6f636b732e00b8456d69747320616e205b604576656e743a3a457874656e646564605d206576656e74206f6e20737563636573732eec4572726f72732077697468205b604572726f723a3a457869746564605d2069662074686520736166652d6d6f646520697320696e6163746976652e00fc43616e206f6e6c792062652063616c6c656420627920746865205b60436f6e6669673a3a466f726365457874656e644f726967696e605d206f726967696e2e28666f7263655f65786974000424604578697420736166652d6d6f646520627920666f7263652e001d01456d69747320616e205b604576656e743a3a457869746564605d2077697468205b6045786974526561736f6e3a3a466f726365605d206576656e74206f6e20737563636573732eec4572726f72732077697468205b604572726f723a3a457869746564605d2069662074686520736166652d6d6f646520697320696e6163746976652e0055014e6f74653a2060736166652d6d6f6465602077696c6c206265206175746f6d61746963616c6c79206465616374697661746564206279205b6050616c6c65743a3a6f6e5f696e697469616c697a65605d20686f6f6b250161667465722074686520626c6f636b206865696768742069732067726561746572207468616e20746865205b60456e7465726564556e74696c605d2073746f72616765206974656d2e5501456d69747320616e205b604576656e743a3a457869746564605d2077697468205b6045786974526561736f6e3a3a54696d656f7574605d206576656e74207768656e20646561637469766174656420696e2074686514686f6f6b2e4c666f7263655f736c6173685f6465706f73697408011c6163636f756e74000130543a3a4163636f756e744964000114626c6f636b100144426c6f636b4e756d626572466f723c543e0005243101536c6173682061206465706f73697420666f7220616e206163636f756e74207468617420656e7465726564206f7220657874656e64656420736166652d6d6f6465206174206120676976656e44686973746f726963616c20626c6f636b2e00cc546869732063616e206f6e6c792062652063616c6c6564207768696c6520736166652d6d6f646520697320656e74657265642e00cc456d6974732061205b604576656e743a3a4465706f736974536c6173686564605d206576656e74206f6e20737563636573732edc4572726f72732077697468205b604572726f723a3a456e7465726564605d20696620736166652d6d6f646520697320656e74657265642e00010143616e206f6e6c792062652063616c6c656420627920746865205b60436f6e6669673a3a466f7263654465706f7369744f726967696e605d206f726967696e2e3c72656c656173655f6465706f73697408011c6163636f756e74000130543a3a4163636f756e744964000114626c6f636b100144426c6f636b4e756d626572466f723c543e00063035015065726d697373696f6e6c6573736c792072656c656173652061206465706f73697420666f7220616e206163636f756e74207468617420656e746572656420736166652d6d6f646520617420615c676976656e20686973746f726963616c20626c6f636b2e0049015468652063616c6c2063616e20626520636f6d706c6574656c792064697361626c65642062792073657474696e67205b60436f6e6669673a3a52656c6561736544656c6179605d20746f20604e6f6e65602ef8546869732063616e6e6f742062652063616c6c6564207768696c6520736166652d6d6f646520697320656e746572656420616e64206e6f7420756e74696c21015b60436f6e6669673a3a52656c6561736544656c6179605d20626c6f636b732068617665207061737365642073696e636520736166652d6d6f64652077617320656e74657265642e00d0456d6974732061205b604576656e743a3a4465706f73697452656c6561736564605d206576656e74206f6e20737563636573732eec4572726f72732077697468205b604572726f723a3a456e7465726564605d2069662074686520736166652d6d6f646520697320656e74657265642e49014572726f72732077697468205b604572726f723a3a43616e6e6f7452656c65617365596574605d206966205b60436f6e6669673a3a52656c6561736544656c6179605d20626c6f636b2068617665206e6f7461017061737365642073696e636520736166652d6d6f64652077617320656e74657265642e204572726f72732077697468205b604572726f723a3a4e6f4465706f736974605d2069662074686520706179656520686173206e6fa472657365727665642063757272656e63792061742074686520626c6f636b207370656369666965642e54666f7263655f72656c656173655f6465706f73697408011c6163636f756e74000130543a3a4163636f756e744964000114626c6f636b100144426c6f636b4e756d626572466f723c543e00072c2d01466f72636520746f2072656c656173652061206465706f73697420666f7220616e206163636f756e74207468617420656e746572656420736166652d6d6f6465206174206120676976656e44686973746f726963616c20626c6f636b2e00d0546869732063616e2062652063616c6c6564207768696c6520736166652d6d6f6465206973207374696c6c20656e74657265642e00d0456d6974732061205b604576656e743a3a4465706f73697452656c6561736564605d206576656e74206f6e20737563636573732edc4572726f72732077697468205b604572726f723a3a456e7465726564605d20696620736166652d6d6f646520697320656e74657265642e35014572726f72732077697468205b604572726f723a3a4e6f4465706f736974605d2069662074686520706179656520686173206e6f2072657365727665642063757272656e6379206174207468654073706563696669656420626c6f636b2e00010143616e206f6e6c792062652063616c6c656420627920746865205b60436f6e6669673a3a466f7263654465706f7369744f726967696e605d206f726967696e2e040d01436f6e7461696e7320612076617269616e742070657220646973706174636861626c652065787472696e736963207468617420746869732070616c6c6574206861732e85050c3c70616c6c65745f657468657265756d1870616c6c65741043616c6c040454000104207472616e7361637404012c7472616e73616374696f6e8905012c5472616e73616374696f6e000004845472616e7361637420616e20457468657265756d207472616e73616374696f6e2e040d01436f6e7461696e7320612076617269616e742070657220646973706174636861626c652065787472696e736963207468617420746869732070616c6c6574206861732e89050c20657468657265756d2c7472616e73616374696f6e345472616e73616374696f6e563200010c184c656761637904008d0501444c65676163795472616e73616374696f6e0000001c4549503239333004009d050148454950323933305472616e73616374696f6e0001001c454950313535390400a9050148454950313535395472616e73616374696f6e000200008d050c20657468657265756d2c7472616e73616374696f6e444c65676163795472616e73616374696f6e00001c01146e6f6e636541010110553235360001246761735f707269636541010110553235360001246761735f6c696d69744101011055323536000118616374696f6e910501445472616e73616374696f6e416374696f6e00011476616c75654101011055323536000114696e70757438011442797465730001247369676e6174757265950501505472616e73616374696f6e5369676e6174757265000091050c20657468657265756d2c7472616e73616374696f6e445472616e73616374696f6e416374696f6e0001081043616c6c04000d01011048313630000000184372656174650001000095050c20657468657265756d2c7472616e73616374696f6e505472616e73616374696f6e5369676e617475726500000c010476990501545472616e73616374696f6e5265636f76657279496400010472340110483235360001047334011048323536000099050c20657468657265756d2c7472616e73616374696f6e545472616e73616374696f6e5265636f7665727949640000040018010c75363400009d050c20657468657265756d2c7472616e73616374696f6e48454950323933305472616e73616374696f6e00002c0120636861696e5f696418010c7536340001146e6f6e636541010110553235360001246761735f707269636541010110553235360001246761735f6c696d69744101011055323536000118616374696f6e910501445472616e73616374696f6e416374696f6e00011476616c75654101011055323536000114696e707574380114427974657300012c6163636573735f6c697374a10501284163636573734c6973740001306f64645f795f706172697479240110626f6f6c000104723401104832353600010473340110483235360000a105000002a50500a5050c20657468657265756d2c7472616e73616374696f6e384163636573734c6973744974656d000008011c616464726573730d01011c4164647265737300013073746f726167655f6b657973b401245665633c483235363e0000a9050c20657468657265756d2c7472616e73616374696f6e48454950313535395472616e73616374696f6e0000300120636861696e5f696418010c7536340001146e6f6e636541010110553235360001606d61785f7072696f726974795f6665655f7065725f676173410101105532353600013c6d61785f6665655f7065725f67617341010110553235360001246761735f6c696d69744101011055323536000118616374696f6e910501445472616e73616374696f6e416374696f6e00011476616c75654101011055323536000114696e707574380114427974657300012c6163636573735f6c697374a10501284163636573734c6973740001306f64645f795f706172697479240110626f6f6c000104723401104832353600010473340110483235360000ad050c2870616c6c65745f65766d1870616c6c65741043616c6c04045400011820776974686472617708011c616464726573730d0101104831363000011476616c756518013042616c616e63654f663c543e000004e057697468647261772062616c616e63652066726f6d2045564d20696e746f2063757272656e63792f62616c616e6365732070616c6c65742e1063616c6c240118736f757263650d010110483136300001187461726765740d01011048313630000114696e70757438011c5665633c75383e00011476616c756541010110553235360001246761735f6c696d697418010c75363400013c6d61785f6665655f7065725f67617341010110553235360001606d61785f7072696f726974795f6665655f7065725f676173b10501304f7074696f6e3c553235363e0001146e6f6e6365b10501304f7074696f6e3c553235363e00012c6163636573735f6c697374b50501585665633c28483136302c205665633c483235363e293e0001045d01497373756520616e2045564d2063616c6c206f7065726174696f6e2e20546869732069732073696d696c617220746f2061206d6573736167652063616c6c207472616e73616374696f6e20696e20457468657265756d2e18637265617465200118736f757263650d01011048313630000110696e697438011c5665633c75383e00011476616c756541010110553235360001246761735f6c696d697418010c75363400013c6d61785f6665655f7065725f67617341010110553235360001606d61785f7072696f726974795f6665655f7065725f676173b10501304f7074696f6e3c553235363e0001146e6f6e6365b10501304f7074696f6e3c553235363e00012c6163636573735f6c697374b50501585665633c28483136302c205665633c483235363e293e0002085101497373756520616e2045564d20637265617465206f7065726174696f6e2e20546869732069732073696d696c617220746f206120636f6e7472616374206372656174696f6e207472616e73616374696f6e20696e24457468657265756d2e1c63726561746532240118736f757263650d01011048313630000110696e697438011c5665633c75383e00011073616c743401104832353600011476616c756541010110553235360001246761735f6c696d697418010c75363400013c6d61785f6665655f7065725f67617341010110553235360001606d61785f7072696f726974795f6665655f7065725f676173b10501304f7074696f6e3c553235363e0001146e6f6e6365b10501304f7074696f6e3c553235363e00012c6163636573735f6c697374b50501585665633c28483136302c205665633c483235363e293e0003047c497373756520616e2045564d2063726561746532206f7065726174696f6e2e347365745f77686974656c69737404010c6e6577bd0501245665633c483136303e0004004464697361626c655f77686974656c69737404012064697361626c6564240110626f6f6c000500040d01436f6e7461696e7320612076617269616e742070657220646973706174636861626c652065787472696e736963207468617420746869732070616c6c6574206861732eb10504184f7074696f6e0404540141010108104e6f6e6500000010536f6d65040041010000010000b505000002b90500b905000004080d01b400bd050000020d0100c1050c4870616c6c65745f64796e616d69635f6665651870616c6c65741043616c6c040454000104646e6f74655f6d696e5f6761735f70726963655f7461726765740401187461726765744101011055323536000000040d01436f6e7461696e7320612076617269616e742070657220646973706174636861626c652065787472696e736963207468617420746869732070616c6c6574206861732ec5050c3c70616c6c65745f626173655f6665651870616c6c65741043616c6c040454000108507365745f626173655f6665655f7065725f67617304010c6665654101011055323536000000387365745f656c6173746963697479040128656c61737469636974794901011c5065726d696c6c000100040d01436f6e7461696e7320612076617269616e742070657220646973706174636861626c652065787472696e736963207468617420746869732070616c6c6574206861732ec9050c3070616c6c65745f6472616e641870616c6c65741043616c6c0404540001082c77726974655f70756c736508013870756c7365735f7061796c6f6164cd0501ac50756c7365735061796c6f61643c543a3a5075626c69632c20426c6f636b4e756d626572466f723c543e3e0001247369676e6174757265e50501504f7074696f6e3c543a3a5369676e61747572653e000004e456657269667920616e6420777269746520612070756c73652066726f6d2074686520626561636f6e20696e746f207468652072756e74696d65447365745f626561636f6e5f636f6e666967080138636f6e6669675f7061796c6f6164ed0501e0426561636f6e436f6e66696775726174696f6e5061796c6f61643c543a3a5075626c69632c20426c6f636b4e756d626572466f723c543e3e0001247369676e6174757265e50501504f7074696f6e3c543a3a5369676e61747572653e000118d0616c6c6f77732074686520726f6f74207573657220746f207365742074686520626561636f6e20636f6e66696775726174696f6efc67656e6572616c6c79207468697320776f756c642062652063616c6c65642066726f6d20616e206f6666636861696e20776f726b657220636f6e746578742e11017468657265206973206e6f20766572696669636174696f6e206f6620636f6e66696775726174696f6e732c20736f206265206361726566756c207769746820746869732e00642a20606f726967696e603a2074686520726f6f742075736572902a2060636f6e666967603a2074686520626561636f6e20636f6e66696775726174696f6e040d01436f6e7461696e7320612076617269616e742070657220646973706174636861626c652065787472696e736963207468617420746869732070616c6c6574206861732ecd050c3070616c6c65745f6472616e641474797065733450756c7365735061796c6f616408185075626c696301d1052c426c6f636b4e756d6265720110000c0130626c6f636b5f6e756d62657210012c426c6f636b4e756d62657200011870756c736573d50501285665633c50756c73653e0001187075626c6963d10501185075626c69630000d105082873705f72756e74696d652c4d756c74695369676e657200010c1c45643235353139040004013c656432353531393a3a5075626c69630000001c53723235353139040004013c737232353531393a3a5075626c69630001001445636473610400e503013465636473613a3a5075626c696300020000d505000002d90500d9050c3070616c6c65745f6472616e641474797065731450756c736500000c0114726f756e6418012c526f756e644e756d62657200012872616e646f6d6e657373dd050170426f756e6465645665633c75382c20436f6e73745533323c33323e3e0001247369676e6174757265e1050174426f756e6465645665633c75382c20436f6e73745533323c3134343e3e0000dd050c4c626f756e6465645f636f6c6c656374696f6e732c626f756e6465645f76656328426f756e64656456656308045401080453000004003801185665633c543e0000e1050c4c626f756e6465645f636f6c6c656374696f6e732c626f756e6465645f76656328426f756e64656456656308045401080453000004003801185665633c543e0000e50504184f7074696f6e04045401e9050108104e6f6e6500000010536f6d650400e9050000010000e905082873705f72756e74696d65384d756c74695369676e617475726500010c1c456432353531390400ed010148656432353531393a3a5369676e61747572650000001c537232353531390400ed010148737232353531393a3a5369676e617475726500010014456364736104007904014065636473613a3a5369676e617475726500020000ed050c3070616c6c65745f6472616e6414747970657368426561636f6e436f6e66696775726174696f6e5061796c6f616408185075626c696301d1052c426c6f636b4e756d6265720110000c0130626c6f636b5f6e756d62657210012c426c6f636b4e756d626572000118636f6e666967f105014c426561636f6e436f6e66696775726174696f6e0001187075626c6963d10501185075626c69630000f1050c3070616c6c65745f6472616e641474797065734c426561636f6e436f6e66696775726174696f6e00001c01287075626c69635f6b6579f505013c4f70617175655075626c69634b6579000118706572696f6410010c75333200013067656e657369735f74696d6510010c75333200011068617368dd05012c426f756e6465644861736800012867726f75705f68617368dd05012c426f756e64656448617368000124736368656d655f6964dd05012c426f756e646564486173680001206d65746164617461f90501204d657461646174610000f5050c4c626f756e6465645f636f6c6c656374696f6e732c626f756e6465645f76656328426f756e64656456656308045401080453000004003801185665633c543e0000f9050c3070616c6c65745f6472616e64147479706573204d657461646174610000040124626561636f6e5f6964dd05012c426f756e646564486173680000fd0504184f7074696f6e04045401dd020108104e6f6e6500000010536f6d650400dd02000001000001060c4070616c6c65745f73756274656e736f721870616c6c6574144572726f7204045400014d01585375624e6574776f726b446f65734e6f74457869737400000468546865207375626e657420646f6573206e6f742065786973742e5c526f6f744e6574776f726b446f65734e6f7445786973740001048054686520726f6f74206e6574776f726b20646f6573206e6f742065786973742e34496e76616c69644970547970650002043901546865207573657220697320747279696e6720746f20736572766520616e2061786f6e207768696368206973206e6f74206f662074797065203420284950763429206f722036202849507636292e40496e76616c6964497041646472657373000304d8416e20696e76616c696420495020616464726573732069732070617373656420746f207468652073657276652066756e6374696f6e2e2c496e76616c6964506f7274000404c0416e20696e76616c696420706f72742069732070617373656420746f207468652073657276652066756e6374696f6e2e6c486f744b65794e6f7452656769737465726564496e5375624e65740005049854686520686f746b6579206973206e6f74207265676973746572656420696e207375626e657458486f744b65794163636f756e744e6f744578697374730006046854686520686f746b657920646f6573206e6f742065786973747370486f744b65794e6f7452656769737465726564496e4e6574776f726b000704ac54686520686f746b6579206973206e6f74207265676973746572656420696e20616e79207375626e65742e504e6f6e4173736f636961746564436f6c644b65790008085d015265717565737420746f207374616b652c20756e7374616b65206f7220737562736372696265206973206d616465206279206120636f6c646b65792074686174206973206e6f74206173736f63696174656420776974684c74686520686f746b6579206163636f756e742e384e6f74456e6f7567685374616b65000908b4444550524543415445443a205374616b6520616d6f756e7420746f207769746864726177206973207a65726f2ef85468652063616c6c657220646f6573206e6f74206861766520656e6f75676874207374616b6520746f20706572666f726d207468697320616374696f6e2e604e6f74456e6f7567685374616b65546f5769746864726177000a0859015468652063616c6c65722069732072657175657374696e672072656d6f76696e67206d6f7265207374616b65207468616e2074686572652065786973747320696e20746865207374616b696e67206163636f756e742e605365653a20225b72656d6f76655f7374616b6528295d222e684e6f74456e6f7567685374616b65546f53657457656967687473000b0849015468652063616c6c65722069732072657175657374696e6720746f20736574207765696768747320627574207468652063616c6c657220686173206c657373207468616e206d696e696d756d207374616b65d0726571756972656420746f20736574207765696768747320286c657373207468616e20576569676874734d696e5374616b65292e704e6f74456e6f7567685374616b65546f5365744368696c646b657973000c04050154686520706172656e7420686f746b657920646f65736e2774206861766520656e6f756768206f776e207374616b6520746f20736574206368696c646b6579732e5c4e6f74456e6f75676842616c616e6365546f5374616b65000d0851015468652063616c6c65722069732072657175657374696e6720616464696e67206d6f7265207374616b65207468616e2074686572652065786973747320696e2074686520636f6c646b6579206163636f756e742e505365653a20225b6164645f7374616b6528295d225842616c616e63655769746864726177616c4572726f72000e0861015468652063616c6c657220697320747279696e6720746f20616464207374616b652c2062757420666f7220736f6d6520726561736f6e207468652072657175657374656420616d6f756e7420636f756c64206e6f742062658c77697468647261776e2066726f6d2074686520636f6c646b6579206163636f756e742e645a65726f42616c616e6365416674657257697468647261776e000f084501556e7375636365737366756c6c792077697468647261772c2062616c616e636520636f756c64206265207a65726f202863616e206e6f74206d616b65206163636f756e74206578697374292061667465722c7769746864726177616c2e5c4e6575726f6e4e6f56616c696461746f725065726d697400100455015468652063616c6c657220697320617474656d7074696e6720746f20736574206e6f6e2d73656c66207765696768747320776974686f7574206265696e672061207065726d69747465642076616c696461746f722e545765696768745665634e6f74457175616c53697a6500110845015468652063616c6c657220697320617474656d7074696e6720746f207365742074686520776569676874206b65797320616e642076616c7565732062757420746865736520766563746f727320686176653c646966666572656e742073697a652e344475706c69636174655569647300120445015468652063616c6c657220697320617474656d7074696e6720746f2073657420776569676874732077697468206475706c6963617465205549447320696e2074686520776569676874206d61747269782e5c556964566563436f6e7461696e496e76616c69644f6e6500130855015468652063616c6c657220697320617474656d7074696e6720746f207365742077656967687420746f206174206c65617374206f6e6520554944207468617420646f6573206e6f7420657869737420696e20746865286d65746167726170682e505765696768745665634c656e67746849734c6f77001404610154686520646973706174636820697320617474656d7074696e6720746f207365742077656967687473206f6e20636861696e207769746820666577657220656c656d656e7473207468616e2061726520616c6c6f7765642e74546f6f4d616e79526567697374726174696f6e7354686973426c6f636b0015084d014e756d626572206f6620726567697374726174696f6e7320696e207468697320626c6f636b20657863656564732074686520616c6c6f776564206e756d6265722028692e652e2c206578636565647320746865b07375626e6574206879706572706172616d6574657220226d61785f726567735f7065725f626c6f636b22292e7c486f744b6579416c726561647952656769737465726564496e5375624e657400160455015468652063616c6c65722069732072657175657374696e67207265676973746572696e672061206e6575726f6e20776869636820616c72656164792065786973747320696e2074686520616374697665207365742e584e6577486f744b6579497353616d65576974684f6c6400170494546865206e657720686f746b6579206973207468652073616d65206173206f6c64206f6e6540496e76616c6964576f726b426c6f636b001804e454686520737570706c69656420506f57206861736820626c6f636b20697320696e2074686520667574757265206f72206e656761746976652e44496e76616c6964446966666963756c7479001904050154686520737570706c69656420506f57206861736820626c6f636b20646f6573206e6f74206d65657420746865206e6574776f726b20646966666963756c74792e2c496e76616c69645365616c001a04f054686520737570706c69656420506f572068617368207365616c20646f6573206e6f74206d617463682074686520737570706c69656420776f726b2e444d61785765696768744578636565646564001b08490154686520646973706174636820697320617474656d7074696e6720746f207365742077656967687473206f6e20636861696e2077697468207765696768742076616c756520657863656564696e6720746865e04d61785765696768744c696d697420286d61785f7765696768745f6c696d6974207375626e6574206879706572706172616d65746572292e54486f744b6579416c726561647944656c6567617465001c04510154686520686f746b657920697320617474656d7074696e6720746f206265636f6d6520612064656c6567617465207768656e2074686520686f746b657920697320616c726561647920612064656c65676174652e5453657474696e6757656967687473546f6f46617374001d04e441207472616e736163746f72206578636565646564207468652072617465206c696d697420666f722073657474696e6720776569676874732e64496e636f727265637457656967687456657273696f6e4b6579001e046101412076616c696461746f7220697320617474656d7074696e6720746f2073657420776569676874732066726f6d20612076616c696461746f72207769746820696e636f7272656374207765696768742076657273696f6e2e6053657276696e67526174654c696d69744578636565646564001f043901416e2061786f6e206f722070726f6d6574686575732073657276696e67206578636565646564207468652072617465206c696d697420666f7220612072656769737465726564206e6575726f6e2e70556964734c656e67746845786365656455696473496e5375624e657400200411015468652063616c6c657220697320617474656d7074696e6720746f2073657420776569676874732077697468206d6f72652055494473207468616e20616c6c6f7765642e684e6574776f726b5478526174654c696d69744578636565646564002104050141207472616e736163746f72206578636565646564207468652072617465206c696d697420666f7220616464206e6574776f726b207472616e73616374696f6e2e6c44656c65676174655478526174654c696d69744578636565646564002204f841207472616e736163746f72206578636565646564207468652072617465206c696d697420666f722064656c6567617465207472616e73616374696f6e2e70486f744b65795365745478526174654c696d69744578636565646564002304110141207472616e736163746f72206578636565646564207468652072617465206c696d697420666f722073657474696e67206f72207377617070696e6720686f746b65792e605374616b696e67526174654c696d69744578636565646564002404c441207472616e736163746f72206578636565646564207468652072617465206c696d697420666f72207374616b696e672e685375624e6574526567697374726174696f6e44697361626c656400250464526567697374726174696f6e2069732064697361626c65642e80546f6f4d616e79526567697374726174696f6e7354686973496e74657276616c0026044101546865206e756d626572206f6620726567697374726174696f6e20617474656d7074732065786365656465642074686520616c6c6f776564206e756d62657220696e2074686520696e74657276616c2e7c5472616e736163746f724163636f756e7453686f756c644265486f744b6579002704a054686520686f746b657920697320726571756972656420746f20626520746865206f726967696e2e3c4e6f7453656e6174654d656d62657200280409014120686f746b657920697320617474656d7074696e6720746f20646f20736f6d657468696e67206f6e6c792073656e617465206d656d626572732063616e20646f2e3846617563657444697361626c65640029044c4661756365742069732064697361626c65642e384e6f745375626e65744f776e6572002a044c4e6f742061207375626e6574206f776e65722e90526567697374726174696f6e4e6f745065726d69747465644f6e526f6f745375626e6574002b04b84f7065726174696f6e206973206e6f74207065726d6974746564206f6e2074686520726f6f74207375626e65742e485374616b65546f6f4c6f77466f72526f6f74002c0415014120686f746b6579207769746820746f6f206c6974746c65207374616b6520697320617474656d7074696e6720746f206a6f696e2074686520726f6f74207375626e65742e54416c6c4e6574776f726b73496e496d6d756e697479002d049c416c6c207375626e6574732061726520696e2074686520696d6d756e69747920706572696f642e7c4e6f74456e6f75676842616c616e6365546f50617953776170486f744b6579002e04a84e6f7420656e6f7567682062616c616e636520746f20706179207377617070696e6720686f746b65792e344e6f74526f6f745375626e6574002f04dc4e657475696420646f6573206e6f74206d6174636820666f722073657474696e6720726f6f74206e6574776f726b20776569676874732e6c43616e4e6f74536574526f6f744e6574776f726b57656967687473003004a443616e206e6f7420736574207765696768747320666f722074686520726f6f74206e6574776f726b2e4c4e6f4e6575726f6e4964417661696c61626c65003104684e6f206e6575726f6e20494420697320617661696c61626c652e4844656c656761746554616b65546f6f4c6f770032046444656c65676174652074616b6520697320746f6f206c6f772e4c44656c656761746554616b65546f6f486967680033046844656c65676174652074616b6520697320746f6f20686967682e504e6f57656967687473436f6d6d6974466f756e6400340861014e6f20636f6d6d697420666f756e6420666f72207468652070726f766964656420686f746b65792b6e657475696420636f6d62696e6174696f6e207768656e20617474656d7074696e6720746f2072657665616c2074686520776569676874732e7c496e76616c696452657665616c436f6d6d6974486173684e6f744d61746368003504d4436f6d6d6974746564206861736820646f6573206e6f7420657175616c20746865206861736865642072657665616c20646174612e4c436f6d6d697452657665616c456e61626c6564003604f0417474656d7074696e6720746f2063616c6c207365745f77656967687473207768656e20636f6d6d69742f72657665616c20697320656e61626c656450436f6d6d697452657665616c44697361626c6564003704c8417474656d7470696e6720746f20636f6d6d69742f72657665616c2077656967687473207768656e2064697361626c65642e48436f756c644e6f744a6f696e53656e617465003804704e6f742061626c6520746f206a6f696e207468652073656e6174652e4c4c6971756964416c70686144697361626c6564003904bc417474656d7074696e6720746f2073657420616c70686120686967682f6c6f77207768696c652064697361626c65643c416c70686148696768546f6f4c6f77003a049c416c706861206869676820697320746f6f206c6f773a20616c7068615f68696768203e20302e3848416c7068614c6f774f75744f6652616e6765003b04ec416c706861206c6f77206973206f7574206f662072616e67653a20616c7068615f6c6f77203e203020262620616c7068615f6c6f77203c20302e3860436f6c644b6579416c72656164794173736f636961746564003c049054686520636f6c646b65792068617320616c7265616479206265656e2073776170706564804e6f74456e6f75676842616c616e6365546f50617953776170436f6c644b6579003d04d454686520636f6c646b65792062616c616e6365206973206e6f7420656e6f75676820746f2070617920666f7220746865207377617058436f6c646b65794973496e4172626974726174696f6e003e047454686520636f6c646b657920697320696e206172626974726174696f6e30496e76616c69644368696c64003f04f4417474656d7074696e6720746f2073657420616e20696e76616c6964206368696c6420666f72206120686f746b6579206f6e2061206e6574776f726b2e384475706c69636174654368696c64004004984475706c6963617465206368696c64207768656e2073657474696e67206368696c6472656e2e4850726f706f7274696f6e4f766572666c6f77004104a850726f706f7274696f6e206f766572666c6f77207768656e2073657474696e67206368696c6472656e2e3c546f6f4d616e794368696c6472656e00420460546f6f206d616e79206368696c6472656e204d415820352e4c5478526174654c696d69744578636565646564004304a044656661756c74207472616e73616374696f6e2072617465206c696d69742065786365656465642e5053776170416c72656164795363686564756c65640044045c5377617020616c7265616479207363686564756c65642e404661696c6564546f5363686564756c65004504586661696c656420746f207377617020636f6c646b6579484e6577436f6c644b65794973486f746b6579004604544e657720636f6c646b657920697320686f746b65794c496e76616c69644368696c646b657954616b65004704644368696c646b65792074616b6520697320696e76616c69642e7c54784368696c646b657954616b65526174654c696d69744578636565646564004804884368696c646b65792074616b652072617465206c696d69742065786365656465642e3c496e76616c69644964656e7469747900490444496e76616c6964206964656e746974792e544d656368616e69736d446f65734e6f744578697374004a040501547279696e6720746f2072656769737465722061207375626e657420696e746f2061206d656368616e69736d207468617420646f6573206e6f742065786973742e4443616e6e6f74556e7374616b654c6f636b004b048c547279696e6720746f20756e7374616b6520796f7572206c6f636b20616d6f756e742e3c5375626e65744e6f74457869737473004c04c0547279696e6720746f20706572666f726d20616374696f6e206f6e206e6f6e2d6578697374656e74207375626e65742e60546f6f4d616e79556e72657665616c6564436f6d6d697473004d04704d6178696d756d20636f6d6d6974206c696d697420726561636865644c45787069726564576569676874436f6d6d6974004e04b4417474656d7074656420746f2072657665616c207765696768747320746861742061726520657870697265642e3852657665616c546f6f4561726c79004f0498417474656d7074656420746f2072657665616c207765696768747320746f6f206561726c792e4c496e7075744c656e67746873556e657175616c0050041d01417474656d7074656420746f2062617463682072657665616c20776569676874732077697468206d69736d61746368656420766563746f7220696e707574206c656e676874732e60436f6d6d697474696e6757656967687473546f6f46617374005104e441207472616e736163746f72206578636565646564207468652072617465206c696d697420666f722073657474696e6720776569676874732e30416d6f756e74546f6f4c6f77005204605374616b6520616d6f756e7420697320746f6f206c6f772e048054686520604572726f726020656e756d206f6620746869732070616c6c65742e05060c4c626f756e6465645f636f6c6c656374696f6e732c626f756e6465645f76656328426f756e6465645665630804540134045300000400b401185665633c543e00000906084470616c6c65745f636f6c6c65637469766514566f74657308244163636f756e74496401002c426c6f636b4e756d626572011000140114696e64657810013450726f706f73616c496e6465780001247468726573686f6c6410012c4d656d626572436f756e74000110617965735d0201385665633c4163636f756e7449643e0001106e6179735d0201385665633c4163636f756e7449643e00010c656e6410012c426c6f636b4e756d62657200000d060c4470616c6c65745f636f6c6c6563746976651870616c6c6574144572726f72080454000449000128244e6f744d656d626572000004944163636f756e74206973206e6f742061206d656d626572206f6620636f6c6c656374697665444475706c696361746550726f706f73616c0001047c4475706c69636174652070726f706f73616c73206e6f7420616c6c6f7765644450726f706f73616c4e6f744578697374730002044c50726f706f73616c206d75737420657869737464496e6465784d69736d6174636850726f706f73616c4861736800030488496e646578206d69736d617463686564207468652070726f706f73616c2068617368344475706c6963617465566f7465000404584475706c696361746520766f74652069676e6f7265645c546f6f4561726c79546f436c6f736550726f706f73616c0005043d015468652063616c6c20746f20636c6f7365207468652070726f706f73616c20776173206d61646520746f6f206561726c792c206265666f72652074686520656e64206f662074686520766f74696e6758546f6f4d616e7941637469766550726f706f73616c73000604fc54686572652063616e206f6e6c792062652061206d6178696d756d206f6620604d617850726f706f73616c7360206163746976652070726f706f73616c732ea050726f706f73616c5765696768744c6573735468616e446973706174636843616c6c576569676874000704d054686520676976656e207765696768742d626f756e6420666f72207468652070726f706f73616c2077617320746f6f206c6f772ea450726f706f73616c4c656e677468426f756e644c6573735468616e50726f706f73616c4c656e677468000804d054686520676976656e206c656e6774682d626f756e6420666f72207468652070726f706f73616c2077617320746f6f206c6f772ea44475726174696f6e4c6f7765725468616e436f6e666967757265644d6f74696f6e4475726174696f6e000904dc54686520676976656e206d6f74696f6e206475726174696f6e20666f72207468652070726f706f73616c2077617320746f6f206c6f772e048054686520604572726f726020656e756d206f6620746869732070616c6c65742e11060c4c626f756e6465645f636f6c6c656374696f6e732c626f756e6465645f76656328426f756e64656456656308045401000453000004005d0201185665633c543e000015060c4470616c6c65745f6d656d626572736869701870616c6c6574144572726f7208045400044900010c34416c72656164794d656d62657200000444416c72656164792061206d656d6265722e244e6f744d656d626572000104344e6f742061206d656d6265722e38546f6f4d616e794d656d6265727300020444546f6f206d616e79206d656d626572732e048054686520604572726f726020656e756d206f6620746869732070616c6c65742e19060c4c626f756e6465645f636f6c6c656374696f6e732c626f756e6465645f76656328426f756e64656456656308045401000453000004005d0201185665633c543e00001d060c4470616c6c65745f6d656d626572736869701870616c6c6574144572726f7208045400044900010c34416c72656164794d656d62657200000444416c72656164792061206d656d6265722e244e6f744d656d626572000104344e6f742061206d656d6265722e38546f6f4d616e794d656d6265727300020444546f6f206d616e79206d656d626572732e048054686520604572726f726020656e756d206f6620746869732070616c6c65742e21060c3870616c6c65745f7574696c6974791870616c6c6574144572726f7204045400010430546f6f4d616e7943616c6c730000045c546f6f206d616e792063616c6c7320626174636865642e048054686520604572726f726020656e756d206f6620746869732070616c6c65742e25060c2c70616c6c65745f7375646f1870616c6c6574144572726f720404540001042c526571756972655375646f0000048053656e646572206d75737420626520746865205375646f206163636f756e742e04684572726f7220666f7220746865205375646f2070616c6c65742e2906000004080004002d06083c70616c6c65745f6d756c7469736967204d756c7469736967102c426c6f636b4e756d62657201101c42616c616e63650118244163636f756e7449640100304d6178417070726f76616c7300001001107768656ed8015854696d65706f696e743c426c6f636b4e756d6265723e00011c6465706f73697418011c42616c616e63650001246465706f7369746f720001244163636f756e744964000124617070726f76616c733106018c426f756e6465645665633c4163636f756e7449642c204d6178417070726f76616c733e000031060c4c626f756e6465645f636f6c6c656374696f6e732c626f756e6465645f76656328426f756e64656456656308045401000453000004005d0201185665633c543e000035060c3c70616c6c65745f6d756c74697369671870616c6c6574144572726f72040454000138404d696e696d756d5468726573686f6c640000047c5468726573686f6c64206d7573742062652032206f7220677265617465722e3c416c7265616479417070726f766564000104ac43616c6c20697320616c726561647920617070726f7665642062792074686973207369676e61746f72792e444e6f417070726f76616c734e65656465640002049c43616c6c20646f65736e2774206e65656420616e7920286d6f72652920617070726f76616c732e44546f6f4665775369676e61746f72696573000304a854686572652061726520746f6f20666577207369676e61746f7269657320696e20746865206c6973742e48546f6f4d616e795369676e61746f72696573000404ac54686572652061726520746f6f206d616e79207369676e61746f7269657320696e20746865206c6973742e545369676e61746f726965734f75744f664f726465720005040d01546865207369676e61746f7269657320776572652070726f7669646564206f7574206f66206f726465723b20746865792073686f756c64206265206f7264657265642e4c53656e646572496e5369676e61746f726965730006040d015468652073656e6465722077617320636f6e7461696e656420696e20746865206f74686572207369676e61746f726965733b2069742073686f756c646e27742062652e204e6f74466f756e64000704dc4d756c7469736967206f7065726174696f6e206e6f7420666f756e64207768656e20617474656d7074696e6720746f2063616e63656c2e204e6f744f776e65720008042d014f6e6c7920746865206163636f756e742074686174206f726967696e616c6c79206372656174656420746865206d756c74697369672069732061626c6520746f2063616e63656c2069742e2c4e6f54696d65706f696e740009041d014e6f2074696d65706f696e742077617320676976656e2c2079657420746865206d756c7469736967206f7065726174696f6e20697320616c726561647920756e6465727761792e3857726f6e6754696d65706f696e74000a042d014120646966666572656e742074696d65706f696e742077617320676976656e20746f20746865206d756c7469736967206f7065726174696f6e207468617420697320756e6465727761792e4c556e657870656374656454696d65706f696e74000b04f4412074696d65706f696e742077617320676976656e2c20796574206e6f206d756c7469736967206f7065726174696f6e20697320756e6465727761792e3c4d6178576569676874546f6f4c6f77000c04d0546865206d6178696d756d2077656967687420696e666f726d6174696f6e2070726f76696465642077617320746f6f206c6f772e34416c726561647953746f726564000d04a0546865206461746120746f2062652073746f72656420697320616c72656164792073746f7265642e048054686520604572726f726020656e756d206f6620746869732070616c6c65742e3906083c70616c6c65745f707265696d616765404f6c645265717565737453746174757308244163636f756e74496401001c42616c616e6365011801082c556e72657175657374656408011c6465706f7369743d060150284163636f756e7449642c2042616c616e63652900010c6c656e10010c753332000000245265717565737465640c011c6465706f736974410601704f7074696f6e3c284163636f756e7449642c2042616c616e6365293e000114636f756e7410010c75333200010c6c656ed101012c4f7074696f6e3c7533323e000100003d0600000408001800410604184f7074696f6e040454013d060108104e6f6e6500000010536f6d6504003d0600000100004506083c70616c6c65745f707265696d616765345265717565737453746174757308244163636f756e7449640100185469636b657401490601082c556e7265717565737465640801187469636b65744d06014c284163636f756e7449642c205469636b65742900010c6c656e10010c753332000000245265717565737465640c01306d617962655f7469636b65745106016c4f7074696f6e3c284163636f756e7449642c205469636b6574293e000114636f756e7410010c7533320001246d617962655f6c656ed101012c4f7074696f6e3c7533323e00010000490614346672616d655f737570706f72741874726169747318746f6b656e732066756e6769626c6544486f6c64436f6e73696465726174696f6e1404410004460004520004440008467000000400180128463a3a42616c616e636500004d060000040800490600510604184f7074696f6e040454014d060108104e6f6e6500000010536f6d6504004d06000001000055060000040834100059060c4c626f756e6465645f636f6c6c656374696f6e732c626f756e6465645f76656328426f756e64656456656308045401080453000004003801185665633c543e00005d060c3c70616c6c65745f707265696d6167651870616c6c6574144572726f7204045400012018546f6f426967000004a0507265696d61676520697320746f6f206c6172676520746f2073746f7265206f6e2d636861696e2e30416c72656164794e6f746564000104a4507265696d6167652068617320616c7265616479206265656e206e6f746564206f6e2d636861696e2e344e6f74417574686f72697a6564000204c85468652075736572206973206e6f7420617574686f72697a656420746f20706572666f726d207468697320616374696f6e2e204e6f744e6f746564000304fc54686520707265696d6167652063616e6e6f742062652072656d6f7665642073696e636520697420686173206e6f7420796574206265656e206e6f7465642e2452657175657374656400040409014120707265696d616765206d6179206e6f742062652072656d6f766564207768656e20746865726520617265206f75747374616e64696e672072657175657374732e304e6f745265717565737465640005042d0154686520707265696d61676520726571756573742063616e6e6f742062652072656d6f7665642073696e6365206e6f206f75747374616e64696e672072657175657374732065786973742e1c546f6f4d616e7900060455014d6f7265207468616e20604d41585f484153485f555047524144455f42554c4b5f434f554e54602068617368657320776572652072657175657374656420746f206265207570677261646564206174206f6e63652e18546f6f466577000704e4546f6f206665772068617368657320776572652072657175657374656420746f2062652075706772616465642028692e652e207a65726f292e048054686520604572726f726020656e756d206f6620746869732070616c6c65742e61060c4c626f756e6465645f636f6c6c656374696f6e732c626f756e6465645f76656328426f756e646564566563080454016506045300000400790601185665633c543e0000650604184f7074696f6e0404540169060108104e6f6e6500000010536f6d650400690600000100006906084070616c6c65745f7363686564756c6572245363686564756c656414104e616d6501041043616c6c016d062c426c6f636b4e756d62657201103450616c6c6574734f726967696e012d03244163636f756e7449640100001401206d617962655f6964e801304f7074696f6e3c4e616d653e0001207072696f726974790801487363686564756c653a3a5072696f7269747900011063616c6c6d06011043616c6c0001386d617962655f706572696f646963510301944f7074696f6e3c7363686564756c653a3a506572696f643c426c6f636b4e756d6265723e3e0001186f726967696e2d03013450616c6c6574734f726967696e00006d0610346672616d655f737570706f72741874726169747324707265696d616765731c426f756e6465640804540115030448017106010c184c656761637904011068617368340124483a3a4f757470757400000018496e6c696e65040075060134426f756e646564496e6c696e65000100184c6f6f6b757008011068617368340124483a3a4f757470757400010c6c656e10010c7533320002000071060c2873705f72756e74696d65187472616974732c426c616b6554776f3235360000000075060c4c626f756e6465645f636f6c6c656374696f6e732c626f756e6465645f76656328426f756e64656456656308045401080453000004003801185665633c543e000079060000026506007d06084070616c6c65745f7363686564756c65722c5265747279436f6e6669670418506572696f640110000c0134746f74616c5f72657472696573080108753800012472656d61696e696e670801087538000118706572696f64100118506572696f64000081060c4070616c6c65745f7363686564756c65721870616c6c6574144572726f72040454000114404661696c6564546f5363686564756c65000004644661696c656420746f207363686564756c6520612063616c6c204e6f74466f756e640001047c43616e6e6f742066696e6420746865207363686564756c65642063616c6c2e5c546172676574426c6f636b4e756d626572496e50617374000204a4476976656e2074617267657420626c6f636b206e756d62657220697320696e2074686520706173742e4852657363686564756c654e6f4368616e6765000304f052657363686564756c65206661696c6564206265636175736520697420646f6573206e6f74206368616e6765207363686564756c65642074696d652e144e616d6564000404d0417474656d707420746f207573652061206e6f6e2d6e616d65642066756e6374696f6e206f6e2061206e616d6564207461736b2e048054686520604572726f726020656e756d206f6620746869732070616c6c65742e8506000004088906180089060c4c626f756e6465645f636f6c6c656374696f6e732c626f756e6465645f76656328426f756e646564566563080454018d06045300000400910601185665633c543e00008d06083070616c6c65745f70726f78793c50726f7879446566696e6974696f6e0c244163636f756e74496401002450726f78795479706501f02c426c6f636b4e756d6265720110000c012064656c65676174650001244163636f756e74496400012870726f78795f74797065f0012450726f78795479706500011464656c617910012c426c6f636b4e756d626572000091060000028d06009506000004089906180099060c4c626f756e6465645f636f6c6c656374696f6e732c626f756e6465645f76656328426f756e646564566563080454019d06045300000400a10601185665633c543e00009d06083070616c6c65745f70726f787930416e6e6f756e63656d656e740c244163636f756e7449640100104861736801342c426c6f636b4e756d6265720110000c01107265616c0001244163636f756e74496400012463616c6c5f686173683401104861736800011868656967687410012c426c6f636b4e756d6265720000a1060000029d0600a5060c3070616c6c65745f70726f78791870616c6c6574144572726f720404540001201c546f6f4d616e79000004210154686572652061726520746f6f206d616e792070726f786965732072656769737465726564206f7220746f6f206d616e7920616e6e6f756e63656d656e74732070656e64696e672e204e6f74466f756e640001047450726f787920726567697374726174696f6e206e6f7420666f756e642e204e6f7450726f7879000204cc53656e646572206973206e6f7420612070726f7879206f6620746865206163636f756e7420746f2062652070726f786965642e2c556e70726f787961626c650003042101412063616c6c20776869636820697320696e636f6d70617469626c652077697468207468652070726f7879207479706527732066696c7465722077617320617474656d707465642e244475706c69636174650004046c4163636f756e7420697320616c726561647920612070726f78792e304e6f5065726d697373696f6e000504150143616c6c206d6179206e6f74206265206d6164652062792070726f78792062656361757365206974206d617920657363616c617465206974732070726976696c656765732e2c556e616e6e6f756e636564000604d0416e6e6f756e63656d656e742c206966206d61646520617420616c6c2c20776173206d61646520746f6f20726563656e746c792e2c4e6f53656c6650726f78790007046443616e6e6f74206164642073656c662061732070726f78792e048054686520604572726f726020656e756d206f6620746869732070616c6c65742ea9060c3c70616c6c65745f726567697374727914747970657330526567697374726174696f6e081c42616c616e636501184c4d61784164646974696f6e616c4669656c6473000008011c6465706f73697418011c42616c616e6365000110696e666f610301844964656e74697479496e666f3c4d61784164646974696f6e616c4669656c64733e0000ad060c3c70616c6c65745f72656769737472791870616c6c6574144572726f7204045400010c3843616e6e6f74526567697374657200000435014163636f756e7420617474656d7074656420746f20726567697374657220616e206964656e746974792062757420646f6573206e6f74206d6565742074686520726571756972656d656e74732e6c546f6f4d616e794669656c6473496e4964656e74697479496e666f000104ec4163636f756e742070617373656420746f6f206d616e79206164646974696f6e616c206669656c647320746f207468656972206964656e74697479344e6f7452656769737465726564000204a84163636f756e7420646f65736e2774206861766520612072656769737465726564206964656e74697479048054686520604572726f726020656e756d206f6620746869732070616c6c65742eb1060c4870616c6c65745f636f6d6d69746d656e747314747970657330526567697374726174696f6e0c1c42616c616e63650118244d61784669656c6473002c426c6f636b4e756d6265720110000c011c6465706f73697418011c42616c616e6365000114626c6f636b10012c426c6f636b4e756d626572000110696e666f6d040164436f6d6d69746d656e74496e666f3c4d61784669656c64733e0000b5060c4870616c6c65745f636f6d6d69746d656e74731870616c6c6574144572726f7204045400010c74546f6f4d616e794669656c6473496e436f6d6d69746d656e74496e666f000004f44163636f756e742070617373656420746f6f206d616e79206164646974696f6e616c206669656c647320746f20746865697220636f6d6d69746d656e745c4163636f756e744e6f74416c6c6f776564436f6d6d6974000104d44163636f756e74206973206e6f7420616c6c6f7720746f206d616b6520636f6d6d69746d656e747320746f2074686520636861696e78436f6d6d69746d656e74536574526174654c696d69744578636565646564000204f84163636f756e7420697320747279696e6720746f20636f6d6d6974206461746120746f6f20666173742c2072617465206c696d6974206578636565646564048054686520604572726f726020656e756d206f6620746869732070616c6c65742eb9060c4870616c6c65745f61646d696e5f7574696c731870616c6c6574144572726f7204045400010c485375626e6574446f65734e6f744578697374000004d4546865207375626e657420646f6573206e6f742065786973742c20636865636b20746865206e657475696420706172616d65746572784d617856616c696461746f72734c61726765725468616e4d617855496473000104ad01546865206d6178696d756d206e756d626572206f66207375626e65742076616c696461746f7273206d757374206265206c657373207468616e20746865206d6178696d756d206e756d626572206f6620616c6c6f776564205549447320696e20746865207375626e65742e844d6178416c6c6f776564554964734c6573735468616e43757272656e7455496473000204ad01546865206d6178696d756d206e756d626572206f66207375626e65742076616c696461746f7273206d757374206265206d6f7265207468616e207468652063757272656e74206e756d626572206f66205549447320616c726561647920696e20746865207375626e65742e048054686520604572726f726020656e756d206f6620746869732070616c6c65742ebd0600000408001000c10604184f7074696f6e04045401180108104e6f6e6500000010536f6d650400180000010000c5060c4070616c6c65745f736166655f6d6f64651870616c6c6574144572726f7204045400011c1c456e7465726564000004b054686520736166652d6d6f64652069732028616c7265616479206f72207374696c6c2920656e74657265642e18457869746564000104ac54686520736166652d6d6f64652069732028616c7265616479206f72207374696c6c29206578697465642e344e6f74436f6e666967757265640002040901546869732066756e6374696f6e616c697479206f66207468652070616c6c65742069732064697361626c65642062792074686520636f6e66696775726174696f6e2e244e6f4465706f736974000304745468657265206973206e6f2062616c616e63652072657365727665642e40416c72656164794465706f73697465640004045d01546865206163636f756e7420616c7265616479206861732061206465706f73697420726573657276656420616e642063616e207468657265666f7265206e6f7420656e746572206f7220657874656e6420616761696e2e4043616e6e6f7452656c656173655965740005049054686973206465706f7369742063616e6e6f742062652072656c6561736564207965742e3443757272656e63794572726f72000604a0416e206572726f722066726f6d2074686520756e6465726c79696e67206043757272656e6379602e048054686520604572726f726020656e756d206f6620746869732070616c6c65742ec906000002cd0600cd060000040c8905d106e50600d106081866705f727063445472616e73616374696f6e53746174757300001c01407472616e73616374696f6e5f68617368340110483235360001447472616e73616374696f6e5f696e64657810010c75333200011066726f6d0d01011c41646472657373000108746fd506013c4f7074696f6e3c416464726573733e000140636f6e74726163745f61646472657373d506013c4f7074696f6e3c416464726573733e0001106c6f6773d90601205665633c4c6f673e0001286c6f67735f626c6f6f6ddd060114426c6f6f6d0000d50604184f7074696f6e040454010d010108104e6f6e6500000010536f6d6504000d010000010000d906000002390100dd060820657468626c6f6f6d14426c6f6f6d00000400e10601405b75383b20424c4f4f4d5f53495a455d0000e106000003000100000800e5060c20657468657265756d1c726563656970742452656365697074563300010c184c65676163790400e906014445495036353852656365697074446174610000001c454950323933300400e90601484549503239333052656365697074446174610001001c454950313535390400e906014845495031353539526563656970744461746100020000e9060c20657468657265756d1c72656365697074444549503635385265636569707444617461000010012c7374617475735f636f64650801087538000120757365645f67617341010110553235360001286c6f67735f626c6f6f6ddd060114426c6f6f6d0001106c6f6773d90601205665633c4c6f673e0000ed060c20657468657265756d14626c6f636b14426c6f636b040454018905000c0118686561646572f10601184865616465720001307472616e73616374696f6e73f90601185665633c543e0001186f6d6d657273fd06012c5665633c4865616465723e0000f1060c20657468657265756d186865616465721848656164657200003c012c706172656e745f686173683401104832353600012c6f6d6d6572735f686173683401104832353600012c62656e65666963696172790d0101104831363000012873746174655f726f6f74340110483235360001447472616e73616374696f6e735f726f6f743401104832353600013472656365697074735f726f6f74340110483235360001286c6f67735f626c6f6f6ddd060114426c6f6f6d000128646966666963756c747941010110553235360001186e756d62657241010110553235360001246761735f6c696d697441010110553235360001206761735f75736564410101105532353600012474696d657374616d7018010c75363400012865787472615f6461746138011442797465730001206d69785f68617368340110483235360001146e6f6e6365f506010c4836340000f5060c38657468657265756d5f747970657310686173680c48363400000400a501011c5b75383b20385d0000f906000002890500fd06000002f106000107000002e506000507000002d1060009070c3c70616c6c65745f657468657265756d1870616c6c6574144572726f7204045400010840496e76616c69645369676e6174757265000004545369676e617475726520697320696e76616c69642e305072654c6f67457869737473000104d85072652d6c6f672069732070726573656e742c207468657265666f7265207472616e73616374206973206e6f7420616c6c6f7765642e048054686520604572726f726020656e756d206f6620746869732070616c6c65742e0d07082870616c6c65745f65766d30436f64654d65746164617461000008011073697a6518010c753634000110686173683401104832353600001107000004080d01340015070c2870616c6c65745f65766d1870616c6c6574144572726f720404540001382842616c616e63654c6f77000004904e6f7420656e6f7567682062616c616e636520746f20706572666f726d20616374696f6e2c4665654f766572666c6f770001048043616c63756c6174696e6720746f74616c20666565206f766572666c6f7765643c5061796d656e744f766572666c6f770002049043616c63756c6174696e6720746f74616c207061796d656e74206f766572666c6f7765643857697468647261774661696c65640003044c576974686472617720666565206661696c6564384761735072696365546f6f4c6f770004045447617320707269636520697320746f6f206c6f772e30496e76616c69644e6f6e6365000504404e6f6e636520697320696e76616c6964384761734c696d6974546f6f4c6f7700060454476173206c696d697420697320746f6f206c6f772e3c4761734c696d6974546f6f4869676800070458476173206c696d697420697320746f6f20686967682e38496e76616c6964436861696e49640008046054686520636861696e20696420697320696e76616c69642e40496e76616c69645369676e617475726500090464746865207369676e617475726520697320696e76616c69642e285265656e7472616e6379000a043845564d207265656e7472616e6379685472616e73616374696f6e4d757374436f6d6546726f6d454f41000b04244549502d333630372c24556e646566696e6564000c0440556e646566696e6564206572726f722e284e6f74416c6c6f776564000d04bc4f726967696e206973206e6f7420616c6c6f77656420746f20706572666f726d20746865206f7065726174696f6e2e048054686520604572726f726020656e756d206f6620746869732070616c6c65742e19070c3070616c6c65745f6472616e641870616c6c6574144572726f72040454000118244e6f6e6556616c7565000004f85468652076616c7565207265747269657665642077617320604e6f6e6560206173206e6f2076616c7565207761732070726576696f75736c79207365742e3c53746f726167654f766572666c6f770001041d0154686572652077617320616e20617474656d707420746f20696e6372656d656e74207468652076616c756520696e2073746f72616765206f76657220607533323a3a4d4158602e584472616e64436f6e6e656374696f6e4661696c757265000204606661696c656420746f20636f6e6e65637420746f207468653c556e766572696669656450756c7365000304507468652070756c736520697320696e76616c696448496e76616c6964526f756e644e756d6265720004048874686520726f756e64206e756d62657220646964206e6f7420696e6372656d656e745850756c7365566572696669636174696f6e4572726f720005047c7468652070756c736520636f756c64206e6f74206265207665726966696564048054686520604572726f726020656e756d206f6620746869732070616c6c65742e1d070c4466705f73656c665f636f6e7461696e65644c756e636865636b65645f65787472696e73696348556e636865636b656445787472696e736963101c416464726573730155021043616c6c011503245369676e617475726501e9051445787472610121070004005d0701250173705f72756e74696d653a3a67656e657269633a3a556e636865636b656445787472696e7369633c416464726573732c2043616c6c2c205369676e61747572652c2045787472610a3e000021070000042c250729072d07310735073d074107450749075107550700250710306672616d655f73797374656d28657874656e73696f6e7354636865636b5f6e6f6e5f7a65726f5f73656e64657248436865636b4e6f6e5a65726f53656e64657204045400000000290710306672616d655f73797374656d28657874656e73696f6e7348636865636b5f737065635f76657273696f6e40436865636b5370656356657273696f6e040454000000002d0710306672616d655f73797374656d28657874656e73696f6e7340636865636b5f74785f76657273696f6e38436865636b547856657273696f6e04045400000000310710306672616d655f73797374656d28657874656e73696f6e7334636865636b5f67656e6573697330436865636b47656e6573697304045400000000350710306672616d655f73797374656d28657874656e73696f6e733c636865636b5f6d6f7274616c69747938436865636b4d6f7274616c697479040454000004003907010c45726100003907102873705f72756e74696d651c67656e657269630c6572610c4572610001010420496d6d6f7274616c0000001c4d6f7274616c31040008000001001c4d6f7274616c32040008000002001c4d6f7274616c33040008000003001c4d6f7274616c34040008000004001c4d6f7274616c35040008000005001c4d6f7274616c36040008000006001c4d6f7274616c37040008000007001c4d6f7274616c38040008000008001c4d6f7274616c3904000800000900204d6f7274616c313004000800000a00204d6f7274616c313104000800000b00204d6f7274616c313204000800000c00204d6f7274616c313304000800000d00204d6f7274616c313404000800000e00204d6f7274616c313504000800000f00204d6f7274616c313604000800001000204d6f7274616c313704000800001100204d6f7274616c313804000800001200204d6f7274616c313904000800001300204d6f7274616c323004000800001400204d6f7274616c323104000800001500204d6f7274616c323204000800001600204d6f7274616c323304000800001700204d6f7274616c323404000800001800204d6f7274616c323504000800001900204d6f7274616c323604000800001a00204d6f7274616c323704000800001b00204d6f7274616c323804000800001c00204d6f7274616c323904000800001d00204d6f7274616c333004000800001e00204d6f7274616c333104000800001f00204d6f7274616c333204000800002000204d6f7274616c333304000800002100204d6f7274616c333404000800002200204d6f7274616c333504000800002300204d6f7274616c333604000800002400204d6f7274616c333704000800002500204d6f7274616c333804000800002600204d6f7274616c333904000800002700204d6f7274616c343004000800002800204d6f7274616c343104000800002900204d6f7274616c343204000800002a00204d6f7274616c343304000800002b00204d6f7274616c343404000800002c00204d6f7274616c343504000800002d00204d6f7274616c343604000800002e00204d6f7274616c343704000800002f00204d6f7274616c343804000800003000204d6f7274616c343904000800003100204d6f7274616c353004000800003200204d6f7274616c353104000800003300204d6f7274616c353204000800003400204d6f7274616c353304000800003500204d6f7274616c353404000800003600204d6f7274616c353504000800003700204d6f7274616c353604000800003800204d6f7274616c353704000800003900204d6f7274616c353804000800003a00204d6f7274616c353904000800003b00204d6f7274616c363004000800003c00204d6f7274616c363104000800003d00204d6f7274616c363204000800003e00204d6f7274616c363304000800003f00204d6f7274616c363404000800004000204d6f7274616c363504000800004100204d6f7274616c363604000800004200204d6f7274616c363704000800004300204d6f7274616c363804000800004400204d6f7274616c363904000800004500204d6f7274616c373004000800004600204d6f7274616c373104000800004700204d6f7274616c373204000800004800204d6f7274616c373304000800004900204d6f7274616c373404000800004a00204d6f7274616c373504000800004b00204d6f7274616c373604000800004c00204d6f7274616c373704000800004d00204d6f7274616c373804000800004e00204d6f7274616c373904000800004f00204d6f7274616c383004000800005000204d6f7274616c383104000800005100204d6f7274616c383204000800005200204d6f7274616c383304000800005300204d6f7274616c383404000800005400204d6f7274616c383504000800005500204d6f7274616c383604000800005600204d6f7274616c383704000800005700204d6f7274616c383804000800005800204d6f7274616c383904000800005900204d6f7274616c393004000800005a00204d6f7274616c393104000800005b00204d6f7274616c393204000800005c00204d6f7274616c393304000800005d00204d6f7274616c393404000800005e00204d6f7274616c393504000800005f00204d6f7274616c393604000800006000204d6f7274616c393704000800006100204d6f7274616c393804000800006200204d6f7274616c393904000800006300244d6f7274616c31303004000800006400244d6f7274616c31303104000800006500244d6f7274616c31303204000800006600244d6f7274616c31303304000800006700244d6f7274616c31303404000800006800244d6f7274616c31303504000800006900244d6f7274616c31303604000800006a00244d6f7274616c31303704000800006b00244d6f7274616c31303804000800006c00244d6f7274616c31303904000800006d00244d6f7274616c31313004000800006e00244d6f7274616c31313104000800006f00244d6f7274616c31313204000800007000244d6f7274616c31313304000800007100244d6f7274616c31313404000800007200244d6f7274616c31313504000800007300244d6f7274616c31313604000800007400244d6f7274616c31313704000800007500244d6f7274616c31313804000800007600244d6f7274616c31313904000800007700244d6f7274616c31323004000800007800244d6f7274616c31323104000800007900244d6f7274616c31323204000800007a00244d6f7274616c31323304000800007b00244d6f7274616c31323404000800007c00244d6f7274616c31323504000800007d00244d6f7274616c31323604000800007e00244d6f7274616c31323704000800007f00244d6f7274616c31323804000800008000244d6f7274616c31323904000800008100244d6f7274616c31333004000800008200244d6f7274616c31333104000800008300244d6f7274616c31333204000800008400244d6f7274616c31333304000800008500244d6f7274616c31333404000800008600244d6f7274616c31333504000800008700244d6f7274616c31333604000800008800244d6f7274616c31333704000800008900244d6f7274616c31333804000800008a00244d6f7274616c31333904000800008b00244d6f7274616c31343004000800008c00244d6f7274616c31343104000800008d00244d6f7274616c31343204000800008e00244d6f7274616c31343304000800008f00244d6f7274616c31343404000800009000244d6f7274616c31343504000800009100244d6f7274616c31343604000800009200244d6f7274616c31343704000800009300244d6f7274616c31343804000800009400244d6f7274616c31343904000800009500244d6f7274616c31353004000800009600244d6f7274616c31353104000800009700244d6f7274616c31353204000800009800244d6f7274616c31353304000800009900244d6f7274616c31353404000800009a00244d6f7274616c31353504000800009b00244d6f7274616c31353604000800009c00244d6f7274616c31353704000800009d00244d6f7274616c31353804000800009e00244d6f7274616c31353904000800009f00244d6f7274616c3136300400080000a000244d6f7274616c3136310400080000a100244d6f7274616c3136320400080000a200244d6f7274616c3136330400080000a300244d6f7274616c3136340400080000a400244d6f7274616c3136350400080000a500244d6f7274616c3136360400080000a600244d6f7274616c3136370400080000a700244d6f7274616c3136380400080000a800244d6f7274616c3136390400080000a900244d6f7274616c3137300400080000aa00244d6f7274616c3137310400080000ab00244d6f7274616c3137320400080000ac00244d6f7274616c3137330400080000ad00244d6f7274616c3137340400080000ae00244d6f7274616c3137350400080000af00244d6f7274616c3137360400080000b000244d6f7274616c3137370400080000b100244d6f7274616c3137380400080000b200244d6f7274616c3137390400080000b300244d6f7274616c3138300400080000b400244d6f7274616c3138310400080000b500244d6f7274616c3138320400080000b600244d6f7274616c3138330400080000b700244d6f7274616c3138340400080000b800244d6f7274616c3138350400080000b900244d6f7274616c3138360400080000ba00244d6f7274616c3138370400080000bb00244d6f7274616c3138380400080000bc00244d6f7274616c3138390400080000bd00244d6f7274616c3139300400080000be00244d6f7274616c3139310400080000bf00244d6f7274616c3139320400080000c000244d6f7274616c3139330400080000c100244d6f7274616c3139340400080000c200244d6f7274616c3139350400080000c300244d6f7274616c3139360400080000c400244d6f7274616c3139370400080000c500244d6f7274616c3139380400080000c600244d6f7274616c3139390400080000c700244d6f7274616c3230300400080000c800244d6f7274616c3230310400080000c900244d6f7274616c3230320400080000ca00244d6f7274616c3230330400080000cb00244d6f7274616c3230340400080000cc00244d6f7274616c3230350400080000cd00244d6f7274616c3230360400080000ce00244d6f7274616c3230370400080000cf00244d6f7274616c3230380400080000d000244d6f7274616c3230390400080000d100244d6f7274616c3231300400080000d200244d6f7274616c3231310400080000d300244d6f7274616c3231320400080000d400244d6f7274616c3231330400080000d500244d6f7274616c3231340400080000d600244d6f7274616c3231350400080000d700244d6f7274616c3231360400080000d800244d6f7274616c3231370400080000d900244d6f7274616c3231380400080000da00244d6f7274616c3231390400080000db00244d6f7274616c3232300400080000dc00244d6f7274616c3232310400080000dd00244d6f7274616c3232320400080000de00244d6f7274616c3232330400080000df00244d6f7274616c3232340400080000e000244d6f7274616c3232350400080000e100244d6f7274616c3232360400080000e200244d6f7274616c3232370400080000e300244d6f7274616c3232380400080000e400244d6f7274616c3232390400080000e500244d6f7274616c3233300400080000e600244d6f7274616c3233310400080000e700244d6f7274616c3233320400080000e800244d6f7274616c3233330400080000e900244d6f7274616c3233340400080000ea00244d6f7274616c3233350400080000eb00244d6f7274616c3233360400080000ec00244d6f7274616c3233370400080000ed00244d6f7274616c3233380400080000ee00244d6f7274616c3233390400080000ef00244d6f7274616c3234300400080000f000244d6f7274616c3234310400080000f100244d6f7274616c3234320400080000f200244d6f7274616c3234330400080000f300244d6f7274616c3234340400080000f400244d6f7274616c3234350400080000f500244d6f7274616c3234360400080000f600244d6f7274616c3234370400080000f700244d6f7274616c3234380400080000f800244d6f7274616c3234390400080000f900244d6f7274616c3235300400080000fa00244d6f7274616c3235310400080000fb00244d6f7274616c3235320400080000fc00244d6f7274616c3235330400080000fd00244d6f7274616c3235340400080000fe00244d6f7274616c3235350400080000ff00003d070c586e6f64655f73756274656e736f725f72756e74696d652c636865636b5f6e6f6e636528436865636b4e6f6e63650404540000040061010120543a3a4e6f6e63650000410710306672616d655f73797374656d28657874656e73696f6e7330636865636b5f7765696768742c436865636b576569676874040454000000004507086870616c6c65745f7472616e73616374696f6e5f7061796d656e74604368617267655472616e73616374696f6e5061796d656e740404540000040030013042616c616e63654f663c543e00004907084070616c6c65745f73756274656e736f726053756274656e736f725369676e6564457874656e73696f6e040454014d070000004d0708586e6f64655f73756274656e736f725f72756e74696d651c52756e74696d65000000005107084870616c6c65745f636f6d6d69746d656e747368436f6d6d69746d656e74735369676e6564457874656e73696f6e040454014d07000000550708746672616d655f6d657461646174615f686173685f657874656e73696f6e44436865636b4d657461646174614861736804045400000401106d6f6465590701104d6f64650000590708746672616d655f6d657461646174615f686173685f657874656e73696f6e104d6f64650001082044697361626c65640000001c456e61626c6564000100005d07102873705f72756e74696d651c67656e657269634c756e636865636b65645f65787472696e73696348556e636865636b656445787472696e736963101c416464726573730155021043616c6c011503245369676e617475726501e905144578747261012107000400380000006c1853797374656d011853797374656d481c4163636f756e7401010402000ce0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008004e8205468652066756c6c206163636f756e7420696e666f726d6174696f6e20666f72206120706172746963756c6172206163636f756e742049442e3845787472696e736963436f756e74000010040004b820546f74616c2065787472696e7369637320636f756e7420666f72207468652063757272656e7420626c6f636b2e40496e686572656e74734170706c696564010024040004a4205768657468657220616c6c20696e686572656e74732068617665206265656e206170706c6965642e2c426c6f636b576569676874010028180000000000000488205468652063757272656e742077656967687420666f722074686520626c6f636b2e40416c6c45787472696e736963734c656e000010040004410120546f74616c206c656e6774682028696e2062797465732920666f7220616c6c2065787472696e736963732070757420746f6765746865722c20666f72207468652063757272656e7420626c6f636b2e24426c6f636b486173680101040510348000000000000000000000000000000000000000000000000000000000000000000498204d6170206f6620626c6f636b206e756d6265727320746f20626c6f636b206861736865732e3445787472696e736963446174610101040510380400043d012045787472696e73696373206461746120666f72207468652063757272656e7420626c6f636b20286d61707320616e2065787472696e736963277320696e64657820746f206974732064617461292e184e756d6265720100101000000000040901205468652063757272656e7420626c6f636b206e756d626572206265696e672070726f6365737365642e205365742062792060657865637574655f626c6f636b602e28506172656e744861736801003480000000000000000000000000000000000000000000000000000000000000000004702048617368206f66207468652070726576696f757320626c6f636b2e1844696765737401003c040004f020446967657374206f66207468652063757272656e7420626c6f636b2c20616c736f2070617274206f662074686520626c6f636b206865616465722e184576656e747301004c04001ca0204576656e7473206465706f736974656420666f72207468652063757272656e7420626c6f636b2e001d01204e4f54453a20546865206974656d20697320756e626f756e6420616e642073686f756c64207468657265666f7265206e657665722062652072656164206f6e20636861696e2ed020497420636f756c64206f746865727769736520696e666c6174652074686520506f562073697a65206f66206120626c6f636b2e002d01204576656e747320686176652061206c6172676520696e2d6d656d6f72792073697a652e20426f7820746865206576656e747320746f206e6f7420676f206f75742d6f662d6d656d6f7279fc206a75737420696e206361736520736f6d656f6e65207374696c6c207265616473207468656d2066726f6d2077697468696e207468652072756e74696d652e284576656e74436f756e74010010100000000004b820546865206e756d626572206f66206576656e747320696e2074686520604576656e74733c543e60206c6973742e2c4576656e74546f70696373010104023459010400282501204d617070696e67206265747765656e206120746f7069632028726570726573656e74656420627920543a3a486173682920616e64206120766563746f72206f6620696e646578657394206f66206576656e747320696e2074686520603c4576656e74733c543e3e60206c6973742e00510120416c6c20746f70696320766563746f727320686176652064657465726d696e69737469632073746f72616765206c6f636174696f6e7320646570656e64696e67206f6e2074686520746f7069632e2054686973450120616c6c6f7773206c696768742d636c69656e747320746f206c6576657261676520746865206368616e67657320747269652073746f7261676520747261636b696e67206d656368616e69736d20616e64e420696e2063617365206f66206368616e67657320666574636820746865206c697374206f66206576656e7473206f6620696e7465726573742e005901205468652076616c756520686173207468652074797065206028426c6f636b4e756d626572466f723c543e2c204576656e74496e646578296020626563617573652069662077652075736564206f6e6c79206a7573744d012074686520604576656e74496e64657860207468656e20696e20636173652069662074686520746f70696320686173207468652073616d6520636f6e74656e7473206f6e20746865206e65787420626c6f636b0101206e6f206e6f74696669636174696f6e2077696c6c20626520747269676765726564207468757320746865206576656e74206d69676874206265206c6f73742e484c61737452756e74696d655570677261646500005d0104000455012053746f726573207468652060737065635f76657273696f6e6020616e642060737065635f6e616d6560206f66207768656e20746865206c6173742072756e74696d6520757067726164652068617070656e65642e545570677261646564546f553332526566436f756e740100240400044d012054727565206966207765206861766520757067726164656420736f207468617420607479706520526566436f756e74602069732060753332602e2046616c7365202864656661756c7429206966206e6f742e605570677261646564546f547269706c65526566436f756e740100240400085d012054727565206966207765206861766520757067726164656420736f2074686174204163636f756e74496e666f20636f6e7461696e73207468726565207479706573206f662060526566436f756e74602e2046616c736548202864656661756c7429206966206e6f742e38457865637574696f6e506861736500005501040004882054686520657865637574696f6e207068617365206f662074686520626c6f636b2e44417574686f72697a65645570677261646500006501040004b82060536f6d6560206966206120636f6465207570677261646520686173206265656e20617574686f72697a65642e01690101581830426c6f636b576569676874737901f901624d186c000b00409452a30313ffffffffffffffff4247871900010b30beb1555d021366666666666666a6010b0030ef7dba0213ffffffffffffffbf0100004247871900010b30ce562a46031366666666666666e6010b00409452a30313ffffffffffffffff01070010a5d4e8130000000000000040424787190000000004d020426c6f636b20262065787472696e7369637320776569676874733a20626173652076616c75657320616e64206c696d6974732e2c426c6f636b4c656e677468890130000078000000a0000000a00004a820546865206d6178696d756d206c656e677468206f66206120626c6f636b2028696e206279746573292e38426c6f636b48617368436f756e74101060090000045501204d6178696d756d206e756d626572206f6620626c6f636b206e756d62657220746f20626c6f636b2068617368206d617070696e677320746f206b65657020286f6c64657374207072756e6564206669727374292e20446257656967687491014040787d010000000000e1f505000000000409012054686520776569676874206f662072756e74696d65206461746162617365206f7065726174696f6e73207468652072756e74696d652063616e20696e766f6b652e1c56657273696f6e95015104386e6f64652d73756274656e736f72386e6f64652d73756274656e736f7201000000e0000000010000004cdf6acb689907609b0500000037e397fc7c91f5e40200000040fe3ad401f8959a06000000fbc577b9d747efd601000000d2bc9897eed08f1503000000f78b278be53f454c02000000dd718d5cc53262d401000000ab3c0572291feb8b01000000ed99c5acb25eedf503000000bc9d89904f5b923f0100000037c8bb1350a9a2a804000000f3ff14d5ab52705903000000582211f65bb14b8905000000e65b00e46cedd0aa0200000042e62be4a39e5b6001000000806df4ccaa9ed485010000008375104b299b74c5010000005d1fbfbe852f280701000000c6886e2f8e598b0a0100000001000000010484204765742074686520636861696e277320696e2d636f64652076657273696f6e2e28535335385072656669789c082a0014a8205468652064657369676e61746564205353353820707265666978206f66207468697320636861696e2e0039012054686973207265706c6163657320746865202273733538466f726d6174222070726f7065727479206465636c6172656420696e2074686520636861696e20737065632e20526561736f6e20697331012074686174207468652072756e74696d652073686f756c64206b6e6f772061626f7574207468652070726566697820696e206f7264657220746f206d616b6520757365206f662069742061737020616e206964656e746966696572206f662074686520636861696e2e01a901006052616e646f6d6e657373436f6c6c656374697665466c6970016052616e646f6d6e657373436f6c6c656374697665466c6970043852616e646f6d4d6174657269616c0100ad0104000c610120536572696573206f6620626c6f636b20686561646572732066726f6d20746865206c61737420383120626c6f636b73207468617420616374732061732072616e646f6d2073656564206d6174657269616c2e2054686973610120697320617272616e67656420617320612072696e672062756666657220776974682060626c6f636b5f6e756d626572202520383160206265696e672074686520696e64657820696e746f20746865206056656360206f664420746865206f6c6465737420686173682e00000000012454696d657374616d70012454696d657374616d70080c4e6f7701001820000000000000000004a0205468652063757272656e742074696d6520666f72207468652063757272656e7420626c6f636b2e24446964557064617465010024040010d82057686574686572207468652074696d657374616d7020686173206265656e207570646174656420696e207468697320626c6f636b2e00550120546869732076616c7565206973207570646174656420746f206074727565602075706f6e207375636365737366756c207375626d697373696f6e206f6620612074696d657374616d702062792061206e6f64652e4501204974206973207468656e20636865636b65642061742074686520656e64206f66206561636820626c6f636b20657865637574696f6e20696e2074686520606f6e5f66696e616c697a656020686f6f6b2e01b1010004344d696e696d756d506572696f6418207d00000000000000188c20546865206d696e696d756d20706572696f64206265747765656e20626c6f636b732e004d012042652061776172652074686174207468697320697320646966666572656e7420746f20746865202a65787065637465642a20706572696f6420746861742074686520626c6f636b2070726f64756374696f6e4901206170706172617475732070726f76696465732e20596f75722063686f73656e20636f6e73656e7375732073797374656d2077696c6c2067656e6572616c6c7920776f726b2077697468207468697320746f61012064657465726d696e6520612073656e7369626c6520626c6f636b2074696d652e20466f72206578616d706c652c20696e2074686520417572612070616c6c65742069742077696c6c20626520646f75626c6520746869737020706572696f64206f6e2064656661756c742073657474696e67732e00021041757261011041757261082c417574686f7269746965730100b5010400046c205468652063757272656e7420617574686f72697479207365742e2c43757272656e74536c6f740100c1012000000000000000000c80205468652063757272656e7420736c6f74206f66207468697320626c6f636b2e009420546869732077696c6c2062652073657420696e20606f6e5f696e697469616c697a65602e00000430536c6f744475726174696f6e1820fa00000000000000100d012054686520736c6f74206475726174696f6e20417572612073686f756c642072756e20776974682c2065787072657373656420696e206d696c6c697365636f6e64732e3d0120546865206566666563746976652076616c7565206f66207468697320747970652073686f756c64206e6f74206368616e6765207768696c652074686520636861696e2069732072756e6e696e672e00350120466f72206261636b776172647320636f6d7061746962696c6974792065697468657220757365205b604d696e696d756d506572696f6454696d657354776f605d206f72206120636f6e73742e00031c4772616e647061011c4772616e6470611c1453746174650100c50104000490205374617465206f66207468652063757272656e7420617574686f72697479207365742e3450656e64696e674368616e67650000c901040004c42050656e64696e67206368616e67653a20287369676e616c65642061742c207363686564756c6564206368616e6765292e284e657874466f72636564000010040004bc206e65787420626c6f636b206e756d6265722077686572652077652063616e20666f7263652061206368616e67652e1c5374616c6c65640000e40400049020607472756560206966207765206172652063757272656e746c79207374616c6c65642e3043757272656e745365744964010018200000000000000000085d0120546865206e756d626572206f66206368616e6765732028626f746820696e207465726d73206f66206b65797320616e6420756e6465726c79696e672065636f6e6f6d696320726573706f6e736962696c697469657329c420696e20746865202273657422206f66204772616e6470612076616c696461746f72732066726f6d2067656e657369732e30536574496453657373696f6e00010405181004002859012041206d617070696e672066726f6d206772616e6470612073657420494420746f2074686520696e646578206f6620746865202a6d6f737420726563656e742a2073657373696f6e20666f722077686963682069747368206d656d62657273207765726520726573706f6e7369626c652e0045012054686973206973206f6e6c79207573656420666f722076616c69646174696e672065717569766f636174696f6e2070726f6f66732e20416e2065717569766f636174696f6e2070726f6f66206d7573744d0120636f6e7461696e732061206b65792d6f776e6572736869702070726f6f6620666f72206120676976656e2073657373696f6e2c207468657265666f7265207765206e65656420612077617920746f20746965450120746f6765746865722073657373696f6e7320616e64204752414e44504120736574206964732c20692e652e207765206e65656420746f2076616c6964617465207468617420612076616c696461746f7241012077617320746865206f776e6572206f66206120676976656e206b6579206f6e206120676976656e2073657373696f6e2c20616e642077686174207468652061637469766520736574204944207761735420647572696e6720746861742073657373696f6e2e00b82054574f582d4e4f54453a2060536574496460206973206e6f7420756e646572207573657220636f6e74726f6c2e2c417574686f7269746965730100cd0104000484205468652063757272656e74206c697374206f6620617574686f7269746965732e01d501017c0c384d6178417574686f726974696573101020000000045c204d617820417574686f72697469657320696e20757365344d61784e6f6d696e61746f727310101400000004d420546865206d6178696d756d206e756d626572206f66206e6f6d696e61746f727320666f7220656163682076616c696461746f722e584d6178536574496453657373696f6e456e74726965731820000000000000000018390120546865206d6178696d756d206e756d626572206f6620656e747269657320746f206b65657020696e207468652073657420696420746f2073657373696f6e20696e646578206d617070696e672e0031012053696e6365207468652060536574496453657373696f6e60206d6170206973206f6e6c79207573656420666f722076616c69646174696e672065717569766f636174696f6e73207468697329012076616c75652073686f756c642072656c61746520746f2074686520626f6e64696e67206475726174696f6e206f66207768617465766572207374616b696e672073797374656d2069733501206265696e6720757365642028696620616e79292e2049662065717569766f636174696f6e2068616e646c696e67206973206e6f7420656e61626c6564207468656e20746869732076616c7565342063616e206265207a65726f2e010502042042616c616e636573012042616c616e6365731c34546f74616c49737375616e636501001820000000000000000004982054686520746f74616c20756e6974732069737375656420696e207468652073797374656d2e40496e61637469766549737375616e63650100182000000000000000000409012054686520746f74616c20756e697473206f66206f75747374616e64696e672064656163746976617465642062616c616e636520696e207468652073797374656d2e1c4163636f756e74010104020014a000000000000000000000000000000000000000000000000000000000000000000000000000000080600901205468652042616c616e6365732070616c6c6574206578616d706c65206f662073746f72696e67207468652062616c616e6365206f6620616e206163636f756e742e00282023204578616d706c650034206060606e6f636f6d70696c65b02020696d706c2070616c6c65745f62616c616e6365733a3a436f6e66696720666f722052756e74696d65207b19022020202074797065204163636f756e7453746f7265203d2053746f726167654d61705368696d3c53656c663a3a4163636f756e743c52756e74696d653e2c206672616d655f73797374656d3a3a50726f76696465723c52756e74696d653e2c204163636f756e7449642c2053656c663a3a4163636f756e74446174613c42616c616e63653e3e0c20207d102060606000150120596f752063616e20616c736f2073746f7265207468652062616c616e6365206f6620616e206163636f756e7420696e20746865206053797374656d602070616c6c65742e00282023204578616d706c650034206060606e6f636f6d70696c65b02020696d706c2070616c6c65745f62616c616e6365733a3a436f6e66696720666f722052756e74696d65207b7420202074797065204163636f756e7453746f7265203d2053797374656d0c20207d102060606000510120427574207468697320636f6d657320776974682074726164656f6666732c2073746f72696e67206163636f756e742062616c616e63657320696e207468652073797374656d2070616c6c65742073746f7265736d0120606672616d655f73797374656d60206461746120616c6f6e677369646520746865206163636f756e74206461746120636f6e747261727920746f2073746f72696e67206163636f756e742062616c616e63657320696e207468652901206042616c616e636573602070616c6c65742c20776869636820757365732061206053746f726167654d61706020746f2073746f72652062616c616e6365732064617461206f6e6c792e4101204e4f54453a2054686973206973206f6e6c79207573656420696e207468652063617365207468617420746869732070616c6c6574206973207573656420746f2073746f72652062616c616e6365732e144c6f636b7301010402000902040010b820416e79206c6971756964697479206c6f636b73206f6e20736f6d65206163636f756e742062616c616e6365732e2501204e4f54453a2053686f756c64206f6e6c79206265206163636573736564207768656e2073657474696e672c206368616e67696e6720616e642066726565696e672061206c6f636b2e00ad0120557365206f66206c6f636b73206973206465707265636174656420696e206661766f7572206f6620667265657a65732e20536565206068747470733a2f2f6769746875622e636f6d2f706172697479746563682f7375627374726174652f70756c6c2f31323935312f602052657365727665730101040200190204000ca4204e616d6564207265736572766573206f6e20736f6d65206163636f756e742062616c616e6365732e00b10120557365206f66207265736572766573206973206465707265636174656420696e206661766f7572206f6620686f6c64732e20536565206068747470733a2f2f6769746875622e636f6d2f706172697479746563682f7375627374726174652f70756c6c2f31323935312f6014486f6c6473010104020025020400046c20486f6c6473206f6e206163636f756e742062616c616e6365732e1c467265657a6573010104020041020400048820467265657a65206c6f636b73206f6e206163636f756e742062616c616e6365732e015102018c10484578697374656e7469616c4465706f7369741820f40100000000000020410120546865206d696e696d756d20616d6f756e7420726571756972656420746f206b65657020616e206163636f756e74206f70656e2e204d5553542042452047524541544552205448414e205a45524f2100590120496620796f75202a7265616c6c792a206e65656420697420746f206265207a65726f2c20796f752063616e20656e61626c652074686520666561747572652060696e7365637572655f7a65726f5f65646020666f72610120746869732070616c6c65742e20486f77657665722c20796f7520646f20736f20617420796f7572206f776e207269736b3a20746869732077696c6c206f70656e2075702061206d616a6f7220446f5320766563746f722e590120496e206361736520796f752068617665206d756c7469706c6520736f7572636573206f662070726f7669646572207265666572656e6365732c20796f75206d617920616c736f2067657420756e65787065637465648c206265686176696f757220696620796f7520736574207468697320746f207a65726f2e00f020426f74746f6d206c696e653a20446f20796f757273656c662061206661766f757220616e64206d616b65206974206174206c65617374206f6e6521204d61784c6f636b7310103200000010f420546865206d6178696d756d206e756d626572206f66206c6f636b7320746861742073686f756c64206578697374206f6e20616e206163636f756e742edc204e6f74207374726963746c7920656e666f726365642c20627574207573656420666f722077656967687420657374696d6174696f6e2e00ad0120557365206f66206c6f636b73206973206465707265636174656420696e206661766f7572206f6620667265657a65732e20536565206068747470733a2f2f6769746875622e636f6d2f706172697479746563682f7375627374726174652f70756c6c2f31323935312f602c4d617852657365727665731010320000000c0d0120546865206d6178696d756d206e756d626572206f66206e616d656420726573657276657320746861742063616e206578697374206f6e20616e206163636f756e742e00b10120557365206f66207265736572766573206973206465707265636174656420696e206661766f7572206f6620686f6c64732e20536565206068747470733a2f2f6769746875622e636f6d2f706172697479746563682f7375627374726174652f70756c6c2f31323935312f60284d6178467265657a657310103200000004610120546865206d6178696d756d206e756d626572206f6620696e646976696475616c20667265657a65206c6f636b7320746861742063616e206578697374206f6e20616e206163636f756e7420617420616e792074696d652e01650205485472616e73616374696f6e5061796d656e7401485472616e73616374696f6e5061796d656e7408444e6578744665654d756c7469706c6965720100690240000064a7b3b6e00d0000000000000000003853746f7261676556657273696f6e01006d0204000000019404604f7065726174696f6e616c4665654d756c7469706c696572080405545901204120666565206d756c7469706c69657220666f7220604f7065726174696f6e616c602065787472696e7369637320746f20636f6d7075746520227669727475616c207469702220746f20626f6f73742074686569722c20607072696f726974796000510120546869732076616c7565206973206d756c7469706c69656420627920746865206066696e616c5f6665656020746f206f627461696e206120227669727475616c20746970222074686174206973206c61746572f420616464656420746f20612074697020636f6d706f6e656e7420696e20726567756c617220607072696f72697479602063616c63756c6174696f6e732e4d01204974206d65616e732074686174206120604e6f726d616c60207472616e73616374696f6e2063616e2066726f6e742d72756e20612073696d696c61726c792d73697a656420604f7065726174696f6e616c6041012065787472696e736963202877697468206e6f20746970292c20627920696e636c7564696e672061207469702076616c75652067726561746572207468616e20746865207669727475616c207469702e003c20606060727573742c69676e6f726540202f2f20466f7220604e6f726d616c608c206c6574207072696f72697479203d207072696f726974795f63616c6328746970293b0054202f2f20466f7220604f7065726174696f6e616c601101206c6574207669727475616c5f746970203d2028696e636c7573696f6e5f666565202b2074697029202a204f7065726174696f6e616c4665654d756c7469706c6965723bc4206c6574207072696f72697479203d207072696f726974795f63616c6328746970202b207669727475616c5f746970293b1020606060005101204e6f746520746861742073696e636520776520757365206066696e616c5f6665656020746865206d756c7469706c696572206170706c69657320616c736f20746f2074686520726567756c61722060746970605d012073656e74207769746820746865207472616e73616374696f6e2e20536f2c206e6f74206f6e6c7920646f657320746865207472616e73616374696f6e206765742061207072696f726974792062756d702062617365646101206f6e207468652060696e636c7573696f6e5f666565602c2062757420776520616c736f20616d706c6966792074686520696d70616374206f662074697073206170706c69656420746f20604f7065726174696f6e616c6038207472616e73616374696f6e732e00063c53756274656e736f724d6f64756c65013c53756274656e736f724d6f64756c6545026c436f6c646b6579537761705363686564756c654475726174696f6e01001010a08c0000007c446973736f6c76654e6574776f726b5363686564756c654475726174696f6e01001010a08c0000007453656e61746552657175697265645374616b6550657263656e74616765010018200100000000000000002454616f57656967687401001820d9cef753e3a59b043474203d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d70203d3d3d3d205374616b696e67205661726961626c6573203d3d3d3d74203d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d8901205468652053756274656e736f72205b60546f74616c49737375616e6365605d20726570726573656e74732074686520746f74616c2069737375616e6365206f6620746f6b656e73206f6e207468652042697474656e736f72206e6574776f726b2e008020497420697320636f6d707269736564206f662074687265652070617274733a6501202d2054686520746f74616c20616d6f756e74206f662069737375656420746f6b656e732c20747261636b656420696e2074686520546f74616c49737375616e6365206f66207468652042616c616e6365732070616c6c65743501202d2054686520746f74616c20616d6f756e74206f6620746f6b656e73207374616b656420696e207468652073797374656d2c20747261636b656420696e205b60546f74616c5374616b65605d0102202d2054686520746f74616c20616d6f756e74206f6620746f6b656e73206c6f636b656420757020666f72207375626e6574207265672c20747261636b656420696e205b60546f74616c5375626e65744c6f636b6564605d2061747461696e656420627920697465726174696e67206f766572207375626e6574206c6f636b2e007501204576656e7475616c6c792c2042697474656e736f722073686f756c64206d69677261746520746f207573696e6720486f6c647320616674657277686963682074696d652077652077696c6c206e6f742072657175697265207468697354207365706172617465206163636f756e74696e672e6c202d2d2d204954454d202d2d3e20476c6f62616c207765696768743c4d617844656c656761746554616b6501009c08142e048c202d2d2d204954454d20282064656661756c745f64656c65676174655f74616b6520293c4d696e44656c656761746554616b6501009c080000047c202d2d2d204954454d2028206d696e5f64656c65676174655f74616b6520293c4d61784368696c646b657954616b6501009c08142e048c202d2d2d204954454d20282064656661756c745f6368696c646b65795f74616b6520293c4d696e4368696c646b657954616b6501009c080000047c202d2d2d204954454d2028206d696e5f6368696c646b65795f74616b652029144f776e6572010104020000800000000000000000000000000000000000000000000000000000000000000000041501204d4150202820686f742029202d2d3e20636f6c64207c2052657475726e732074686520636f6e74726f6c6c696e6720636f6c646b657920666f72206120686f746b65792e2444656c65676174657301010402009c08142e04b501204d4150202820686f742029202d2d3e2074616b65207c2052657475726e732074686520686f746b65792064656c65676174696f6e2074616b652e20416e64207369676e616c7320746861742074686973206b6579206973206f70656e20666f722064656c65676174696f6e2e304368696c646b657954616b65010108020671029c080000045d0120444d4150202820686f742c206e65747569642029202d2d3e2074616b65207c2052657475726e732074686520686f746b6579206368696c646b65792074616b6520666f722061207370656369666963207375626e65744050656e64696e674368696c644b65797301010806027502790224000000000000000000041d0120444d41502028206e65747569642c20706172656e742029202d2d3e20285665633c2870726f706f7274696f6e2c6368696c64293e2c20636f6f6c5f646f776e5f626c6f636b29244368696c644b65797301010802067102ac040004d020444d4150202820706172656e742c206e65747569642029202d2d3e205665633c2870726f706f7274696f6e2c6368696c64293e28506172656e744b65797301010802067102ac040004d020444d41502028206368696c642c206e65747569642029202d2d3e205665633c2870726f706f7274696f6e2c706172656e74293e5c416c7068614469766964656e64735065725375626e65740101080602750218200000000000000000005454616f4469766964656e64735065725375626e657401010806027502182000000000000000000034426c6f636b456d697373696f6e0100182000ca9a3b00000000104c203d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d4c203d3d3d3d20436f696e62617365203d3d3d3d4c203d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d8c202d2d2d204954454d202820676c6f62616c5f626c6f636b5f656d697373696f6e2029684c617374486f746b6579456d697373696f6e4f6e4e65747569640101080206710218200000000000000000042501202d2d2d20444d6170202820686f742c206e65747569642029202d2d3e20656d697373696f6e207c206c61737420686f746b657920656d697373696f6e206f6e206e6574776f726b2e844c617374486f746b6579436f6c646b6579456d697373696f6e4f6e4e657475696401010c0202067d021820000000000000000004d101202d2d2d204e4d4150202820686f742c20636f6c642c206e65747569642029202d2d3e206c6173745f656d697373696f6e5f6f6e5f686f745f636f6c645f6e6574207c2052657475726e7320746865206c6173745f656d697373696f6e5f7570646174655f6f6e5f686f745f636f6c645f6e657434546f74616c49737375616e6365010018200000000000000000306c203d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d6c203d3d3d3d205374616b696e6720436f756e74657273203d3d3d3d6c203d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d8901205468652053756274656e736f72205b60546f74616c49737375616e6365605d20726570726573656e74732074686520746f74616c2069737375616e6365206f6620746f6b656e73206f6e207468652042697474656e736f72206e6574776f726b2e008020497420697320636f6d707269736564206f662074687265652070617274733a6501202d2054686520746f74616c20616d6f756e74206f662069737375656420746f6b656e732c20747261636b656420696e2074686520546f74616c49737375616e6365206f66207468652042616c616e6365732070616c6c65743501202d2054686520746f74616c20616d6f756e74206f6620746f6b656e73207374616b656420696e207468652073797374656d2c20747261636b656420696e205b60546f74616c5374616b65605d0102202d2054686520746f74616c20616d6f756e74206f6620746f6b656e73206c6f636b656420757020666f72207375626e6574207265672c20747261636b656420696e205b60546f74616c5375626e65744c6f636b6564605d2061747461696e656420627920697465726174696e67206f766572207375626e6574206c6f636b2e007501204576656e7475616c6c792c2042697474656e736f722073686f756c64206d69677261746520746f207573696e6720486f6c647320616674657277686963682074696d652077652077696c6c206e6f742072657175697265207468697354207365706172617465206163636f756e74696e672e28546f74616c5374616b65010018200000000000000000003044796e616d6963426c6f636b01001820000000000000000000305375626e6574566f6c756d65010104069c1820000000000000000000245375626e657454414f010104069c1820000000000000000000545375626e6574416c706861496e456d697373696f6e010104069c1820000000000000000000585375626e6574416c7068614f7574456d697373696f6e010104069c18200000000000000000004c5375626e657454616f496e456d697373696f6e010104069c18200000000000000000005c5375626e6574416c706861456d697373696f6e53656c6c010104069c18200000000000000000004c546f74616c5374616b65417444796e616d6963010104069c1820000000000000000000345375626e6574416c706861496e010104069c1820000000000000000000385375626e6574416c7068614f7574010104069c1820000000000000000000385374616b696e67486f746b65797301010402005d02040000304f776e6564486f746b65797301010402005d02040000145374616b6501010802068102182000000000000000000489012028444550524543415445442920444d4150202820686f742c20636f6c642029202d2d3e207374616b65207c2052657475726e7320746865207374616b6520756e646572206120636f6c646b657920707265666978656420627920686f746b65792e50436f6c646b6579537761705363686564756c65640101040200a4000040546f74616c486f746b6579416c70686101010802067102182000000000000000000044546f74616c486f746b6579536861726573010108020671028502400000000000000000000000000000000004ad0120444d4150202820686f742c206e65747569642029202d2d3e20746f74616c5f616c7068615f736861726573207c2052657475726e7320746865206e756d626572206f6620616c7068612073686172657320666f72206120686f746b6579206f6e2061207375626e65742e14416c70686101010c0202067d0285024000000000000000000000000000000000002c546f6b656e53796d626f6c010104069c381410f09d9c8f00285375626e65744e616d65010104069c381410f09d9c8f002055736564576f726b0101040638182000000000000000001074203d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d74203d3d3d3d20476c6f62616c20506172616d6574657273203d3d3d3d3d74203d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d88202d2d2d2053746f726167654974656d20476c6f62616c205573656420576f726b2e604d6178526567697374726174696f6e73506572426c6f636b010104069c9c08010004bc202d2d2d204954454d2820676c6f62616c5f6d61785f726567697374726174696f6e735f7065725f626c6f636b20292c5375626e65744c696d697401009c080c00049c202d2d2d204954454d28206d6178696d756d5f6e756d6265725f6f665f6e6574776f726b73202934546f74616c4e6574776f726b7301009c08000004b8202d2d2d204954454d2820746f74616c5f6e756d6265725f6f665f6578697374696e675f6e6574776f726b732029544e6574776f726b496d6d756e697479506572696f6401001820e0c40000000000000480204954454d28206e6574776f726b5f696d6d756e6974795f706572696f642029544e6574776f726b4c617374526567697374657265640100182000000000000000000498204954454d28206e6574776f726b5f6c6173745f726567697374657265645f626c6f636b2029544e6574776f726b4d696e416c6c6f7765645569647301009c0880000484204954454d28206e6574776f726b5f6d696e5f616c6c6f7765645f756964732029484e6574776f726b4d696e4c6f636b436f7374010018200010a5d4e80000000478204954454d28206d696e5f6e6574776f726b5f6c6f636b5f636f737420294c4e6574776f726b4c6173744c6f636b436f7374010018200010a5d4e8000000047c204954454d28206c6173745f6e6574776f726b5f6c6f636b5f636f73742029704e6574776f726b4c6f636b526564756374696f6e496e74657276616c01001820c08901000000000004a0204954454d28206e6574776f726b5f6c6f636b5f726564756374696f6e5f696e74657276616c2029385375626e65744f776e657243757401009c08142e0464204954454d28207375626e65745f6f776e65725f6375742029404e6574776f726b526174654c696d6974010018200000000000000000046c204954454d28206e6574776f726b5f726174655f6c696d69742029644e6f6d696e61746f724d696e52657175697265645374616b6501001820000000000000000000305375626e65744c6f636b6564010104069c182000000000000000000c74203d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d60203d3d3d3d205375626e6574204c6f636b73203d3d3d3d3d74203d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d344c6172676573744c6f636b6564010104069c18200000000000000000002041766754656d706f01009c080a000c48203d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d48203d3d3d3d2054656d706f73203d3d3d3d3d48203d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d204d617854656d706f01009c081e00001454656d706f010104069c9c080a00003c5375626e65744d656368616e69736d010104069c9c0800000c74203d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d74203d3d3d3d205375626e657420506172616d6574657273203d3d3d3d3d74203d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d2c5375626e6574776f726b4e010104069c9c080000041501202d2d2d204d41502028206e65747569642029202d2d3e207375626e6574776f726b5f6e20284e756d626572206f66205549447320696e20746865206e6574776f726b292e3c4e6574776f726b4d6f64616c697479010104069c9c08000004fc202d2d2d204d41502028206e65747569642029202d2d3e206d6f64616c697479202020544558543a20302c20494d4147453a20312c2054454e534f523a2032344e6574776f726b734164646564010104069c24040004a0202d2d2d204d41502028206e65747569642029202d2d3e206e6574776f726b5f69735f61646465643c49734e6574776f726b4d656d626572010108020671022404000494202d2d2d20444d4150202820686f746b65792c206e65747569642029202d2d3e20626f6f6c684e6574776f726b526567697374726174696f6e416c6c6f776564010104069c24040004d0202d2d2d204d41502028206e65747569642029202d2d3e206e6574776f726b5f726567697374726174696f6e5f616c6c6f776564744e6574776f726b506f77526567697374726174696f6e416c6c6f776564010104069c24040004ac202d2d2d204d41502028206e65747569642029202d2d3e206e6574776f726b5f706f775f616c6c6f7765644c4e6574776f726b526567697374657265644174010104069c182000000000000000000494202d2d2d204d41502028206e65747569642029202d2d3e20626c6f636b5f6372656174656438456d697373696f6e56616c756573010104069c18200000000000000000049c202d2d2d204d41502028206e65747569642029202d2d3e20656d697373696f6e5f76616c7565733c50656e64696e67456d697373696f6e010104069c1820000000000000000004a0202d2d2d204d41502028206e65747569642029202d2d3e2070656e64696e675f656d697373696f6e3c50656e64696e67526f6f7444697673010104069c1820000000000000000004b4202d2d2d204d41502028206e65747569642029202d2d3e2070656e64696e675f726f6f745f656d697373696f6e4c50656e64696e67416c70686153776170706564010104069c1820000000000000000004b4202d2d2d204d41502028206e65747569642029202d2d3e2070656e64696e675f616c7068615f737761707065643c50656e64696e674f776e6572437574010104069c1820000000000000000004a4202d2d2d204d41502028206e65747569642029202d2d3e2070656e64696e675f6f776e65725f6375744c426c6f636b7353696e63654c61737453746570010104069c1820000000000000000004b8202d2d2d204d41502028206e65747569642029202d2d3e20626c6f636b735f73696e63655f6c6173745f73746570584c6173744d656368616e73696d53746570426c6f636b010104069c1820000000000000000004c4202d2d2d204d41502028206e65747569642029202d2d3e206c6173745f6d656368616e69736d5f737465705f626c6f636b2c5375626e65744f776e6572010104069c008000000000000000000000000000000000000000000000000000000000000000000490202d2d2d204d41502028206e65747569642029202d2d3e207375626e65745f6f776e6572445375626e65744f776e6572486f746b6579010104069c0080000000000000000000000000000000000000000000000000000000000000000004ac202d2d2d204d41502028206e65747569642029202d2d3e207375626e65745f6f776e65725f686f746b65794053657276696e67526174654c696d6974010104069c1820320000000000000004a8202d2d2d204d41502028206e65747569642029202d2d3e2073657276696e675f726174655f6c696d69740c52686f010104069c9c080a00046c202d2d2d204d41502028206e65747569642029202d2d3e2052686f144b61707061010104069c9c08ff7f0474202d2d2d204d41502028206e65747569642029202d2d3e204b61707061644e6575726f6e73546f5072756e6541744e65787445706f6368010104069c9c080000042901202d2d2d204d41502028206e65747569642029202d2d3e207569642c2077652075736520746f207265636f7264207569647320746f207072756e65206174206e6578742065706f63682e64526567697374726174696f6e7354686973496e74657276616c010104069c9c08000004cc202d2d2d204d41502028206e65747569642029202d2d3e20726567697374726174696f6e735f746869735f696e74657276616c70504f57526567697374726174696f6e7354686973496e74657276616c010104069c9c08000004dc202d2d2d204d41502028206e65747569642029202d2d3e20706f775f726567697374726174696f6e735f746869735f696e74657276616c744275726e526567697374726174696f6e7354686973496e74657276616c010104069c9c08000004e0202d2d2d204d41502028206e65747569642029202d2d3e206275726e5f726567697374726174696f6e735f746869735f696e74657276616c384d6178416c6c6f77656455696473010104069c9c08001004a0202d2d2d204d41502028206e65747569642029202d2d3e206d61785f616c6c6f7765645f7569647338496d6d756e697479506572696f64010104069c9c080010049c202d2d2d204d41502028206e65747569642029202d2d3e20696d6d756e6974795f706572696f643841637469766974794375746f6666010104069c9c088813049c202d2d2d204d41502028206e65747569642029202d2d3e2061637469766974795f6375746f66663c4d6178576569676874734c696d6974010104069c9c08e80304a0202d2d2d204d41502028206e65747569642029202d2d3e206d61785f7765696768745f6c696d6974445765696768747356657273696f6e4b6579010104069c1820000000000000000004ac202d2d2d204d41502028206e65747569642029202d2d3e20776569676874735f76657273696f6e5f6b6579444d696e416c6c6f77656457656967687473010104069c9c08000404ac202d2d2d204d41502028206e65747569642029202d2d3e206d696e5f616c6c6f7765645f77656967687473504d6178416c6c6f77656456616c696461746f7273010104069c9c08800004b8202d2d2d204d41502028206e65747569642029202d2d3e206d61785f616c6c6f7765645f76616c696461746f72734841646a7573746d656e74496e74657276616c010104069c9c08640004ac202d2d2d204d41502028206e65747569642029202d2d3e2061646a7573746d656e745f696e74657276616c48426f6e64734d6f76696e6741766572616765010104069c1820a0bb0d000000000004b0202d2d2d204d41502028206e65747569642029202d2d3e20626f6e64735f6d6f76696e675f6176657261676530426f6e647350656e616c7479010104069c9c0800000494202d2d2d204d41502028206e65747569642029202d2d3e20626f6e64735f70656e616c74794c57656967687473536574526174654c696d6974010104069c1820640000000000000004b8202d2d2d204d41502028206e65747569642029202d2d3e20776569676874735f7365745f726174655f6c696d69744456616c696461746f725072756e654c656e010104069c1820010000000000000004ac202d2d2d204d41502028206e65747569642029202d2d3e2076616c696461746f725f7072756e655f6c656e3c5363616c696e674c6177506f776572010104069c9c08320004a4202d2d2d204d41502028206e65747569642029202d2d3e207363616c696e675f6c61775f706f77657278546172676574526567697374726174696f6e73506572496e74657276616c010104069c9c08020004e8202d2d2d204d41502028206e65747569642029202d2d3e207461726765745f726567697374726174696f6e735f746869735f696e74657276616c3c41646a7573746d656e74416c706861010104069c1820000000000000000004a0202d2d2d204d41502028206e65747569642029202d2d3e2061646a7573746d656e745f616c70686168436f6d6d697452657665616c57656967687473456e61626c6564010104069c24040004f0202d2d2d204d41502028206e65747569642029202d2d3e20636f6d6d69742072657665616c20763220776569676874732061726520656e61626c6564104275726e010104069c182000ca9a3b000000000470202d2d2d204d41502028206e65747569642029202d2d3e204275726e28446966666963756c7479010104069c182080969800000000000488202d2d2d204d41502028206e65747569642029202d2d3e20446966666963756c74791c4d696e4275726e010104069c182000ca9a3b00000000047c202d2d2d204d41502028206e65747569642029202d2d3e204d696e4275726e1c4d61784275726e010104069c182000e8764817000000047c202d2d2d204d41502028206e65747569642029202d2d3e204d61784275726e344d696e446966666963756c7479010104069c182080969800000000000494202d2d2d204d41502028206e65747569642029202d2d3e204d696e446966666963756c7479344d6178446966666963756c7479010104069c1820ffffffffffffff3f0494202d2d2d204d41502028206e65747569642029202d2d3e204d6178446966666963756c74794c4c61737441646a7573746d656e74426c6f636b010104069c1820000000000000000004c8202d2d2d204d41502028206e65747569642029202d2d3e2020426c6f636b206174206c6173742061646a7573746d656e742e58526567697374726174696f6e7354686973426c6f636b010104069c9c08000004d0202d2d2d204d41502028206e65747569642029202d2d3e20526567697374726174696f6e73206f66207468697320426c6f636b2e6852414f52656379636c6564466f72526567697374726174696f6e010104069c1820000000000000000004f0202d2d2d204d41502028206e65747569642029202d2d3e20676c6f62616c5f52414f5f72656379636c65645f666f725f726567697374726174696f6e2c5478526174654c696d697401001820e803000000000000046c202d2d2d204954454d20282074785f726174655f6c696d697420295c547844656c656761746554616b65526174654c696d697401001820c04b03000000000004a4202d2d2d204954454d20282074785f64656c65676174655f74616b655f726174655f6c696d697420295c54784368696c646b657954616b65526174654c696d697401001820050000000000000004a4202d2d2d204954454d20282074785f6368696c646b65795f74616b655f726174655f6c696d69742029344c6971756964416c7068614f6e010104029c24040004f8202d2d2d204d41502028206e65747569642029202d2d3e2057686574686572206f72206e6f74204c697175696420416c70686120697320656e61626c65642c416c70686156616c756573010104069cb1021033b366e604b020204d41502028206e65747569642029202d2d3e2028616c7068615f6c6f772c20616c7068615f68696768293c4e6574776f726b4d61785374616b65010104069c1820ffffffffffffffff04c8204d41502028206e65747569642029202d2d3e206d6178207374616b6520616c6c6f776564206f6e2061207375626e65742e2c5374616b65576569676874010104069cb50204000ca0203d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3da0203d3d3d3d205375626e6574776f726b20436f6e73656e7375732053746f7261676520203d3d3d3da0203d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d1055696473000108060275029c04000490202d2d2d20444d41502028206e65747569642c20686f746b65792029202d2d3e20756964104b6579730101080606b102008000000000000000000000000000000000000000000000000000000000000000000490202d2d2d20444d41502028206e65747569642c207569642029202d2d3e20686f746b6579384c6f61646564456d697373696f6e000104069cb902040004a0202d2d2d204d41502028206e65747569642029202d2d3e2028686f746b65792c2073652c2076652918416374697665010104069cc10204000478202d2d2d204d41502028206e65747569642029202d2d3e206163746976651052616e6b010104069cb50204000470202d2d2d204d41502028206e65747569642029202d2d3e2072616e6b145472757374010104069cb50204000474202d2d2d204d41502028206e65747569642029202d2d3e20747275737424436f6e73656e737573010104069cb50204000484202d2d2d204d41502028206e65747569642029202d2d3e20636f6e73656e73757324496e63656e74697665010104069cb50204000484202d2d2d204d41502028206e65747569642029202d2d3e20696e63656e74697665244469766964656e6473010104069cb50204000484202d2d2d204d41502028206e65747569642029202d2d3e206469766964656e647320456d697373696f6e010104069c510104000480202d2d2d204d41502028206e65747569642029202d2d3e20656d697373696f6e284c617374557064617465010104069c51010400048c202d2d2d204d41502028206e65747569642029202d2d3e206c6173745f7570646174653856616c696461746f725472757374010104069cb5020400049c202d2d2d204d41502028206e65747569642029202d2d3e2076616c696461746f725f7472757374345072756e696e6753636f726573010104069cb50204000498202d2d2d204d41502028206e65747569642029202d2d3e207072756e696e675f73636f7265733c56616c696461746f725065726d6974010104069cc102040004a0202d2d2d204d41502028206e65747569642029202d2d3e2076616c696461746f725f7065726d69741c576569676874730101080606b102c50204000494202d2d2d20444d41502028206e65747569642c207569642029202d2d3e207765696768747314426f6e64730101080606b102c5020400048c202d2d2d20444d41502028206e65747569642c207569642029202d2d3e20626f6e64734c426c6f636b4174526567697374726174696f6e0101080606b1021820000000000000000004cc202d2d2d20444d41502028206e65747569642c207569642029202d2d3e20626c6f636b5f61745f726567697374726174696f6e1441786f6e7300010806027502c902040004a4202d2d2d204d41502028206e65747569642c20686f746b65792029202d2d3e2061786f6e5f696e666f484e6575726f6e43657274696669636174657300010806027502cd02040004ac202d2d2d204d41502028206e65747569642c20686f746b65792029202d2d3e2063657274696669636174652850726f6d65746865757300010806027502d502040004bc202d2d2d204d41502028206e65747569642c20686f746b65792029202d2d3e2070726f6d6574686575735f696e666f284964656e7469746965730001040200d902040000405375626e65744964656e746974696573000104029cdd020400005c5472616e73616374696f6e4b65794c617374426c6f636b01010c020606e102182000000000000000000c88203d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d88203d3d3d3d2041786f6e202f2050726f6d6f20456e64706f696e7473203d3d3d3d3d88203d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d2c4c6173745478426c6f636b010104060018200000000000000000047c202d2d2d204d41502028206b65792029202d2d3e206c6173745f626c6f636b5c4c6173745478426c6f636b4368696c644b657954616b6501010406001820000000000000000004c0202d2d2d204d41502028206b65792029202d2d3e206c6173745f74785f626c6f636b5f6368696c646b65795f74616b655c4c6173745478426c6f636b44656c656761746554616b6501010406001820000000000000000004c0202d2d2d204d41502028206b65792029202d2d3e206c6173745f74785f626c6f636b5f64656c65676174655f74616b65385374616b655468726573686f6c640100182000000000000000000468204954454d2820776569676874735f6d696e5f7374616b65202934576569676874436f6d6d69747300010805057502e5020400047902202d2d2d204d415020286e65747569642c2077686f29202d2d3e2056656344657175653c28686173682c20636f6d6d69745f626c6f636b2c2066697273745f72657665616c5f626c6f636b2c206c6173745f72657665616c5f626c6f636b293e207c2053746f7265732061207175657565206f6620636f6d6d69747320666f7220616e206163636f756e74206f6e206120676976656e206e65747569642e4443525633576569676874436f6d6d6974730101080505ed02f1020400048102202d2d2d204d415020286e65747569642c20636f6d6d69745f65706f636829202d2d3e2056656344657175653c2877686f2c2073657269616c697a65645f636f6d707265737365645f636f6d6d69742c2072657665616c5f726f756e64293e207c2053746f7265732061207175657565206f6620763320636f6d6d69747320666f7220616e206163636f756e74206f6e206120676976656e206e65747569642e4852657665616c506572696f6445706f636873010104059c18200100000000000000042101202d2d2d204d617020286e657475696429202d2d3e204e756d626572206f662065706f63687320616c6c6f77656420666f7220636f6d6d69742072657665616c20706572696f64733c4861734d6967726174696f6e52756e01010406382404000c4c203d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d4c203d3d3d3d2047656e65736973203d3d3d3d3d4c203d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d01fd020198d43c496e697469616c49737375616e6365182000000000000000001088203d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d88203d3d3d3d20496e697469616c2056616c756520436f6e7374616e7473203d3d3d3d88203d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d6c20496e697469616c2063757272656e63792069737375616e63652e60496e697469616c4d696e416c6c6f776564576569676874739c080004049420496e697469616c206d696e20616c6c6f77656420776569676874732073657474696e672e50496e697469616c456d697373696f6e56616c75659c080000046020496e697469616c20456d697373696f6e20526174696f2e58496e697469616c4d6178576569676874734c696d69749c08e803046820496e697469616c206d617820776569676874206c696d69742e30496e697469616c54656d706f9c080a0004602054656d706f20666f722065616368206e6574776f726b2e44496e697469616c446966666963756c747918208096980000000000045020496e697469616c20446966666963756c74792e50496e697469616c4d6178446966666963756c74791820ffffffffffffff3f046020496e697469616c204d617820446966666963756c74792e50496e697469616c4d696e446966666963756c747918208096980000000000046020496e697469616c204d696e20446966666963756c74792e84496e697469616c52414f52656379636c6564466f72526567697374726174696f6e18200000000000000000045820496e697469616c2052414f2052656379636c65642e2c496e697469616c4275726e182000ca9a3b00000000043820496e697469616c204275726e2e38496e697469616c4d61784275726e182000e8764817000000044820496e697469616c204d6178204275726e2e38496e697469616c4d696e4275726e182000ca9a3b00000000044820496e697469616c204d696e204275726e2e64496e697469616c41646a7573746d656e74496e74657276616c9c086400047420496e697469616c2061646a7573746d656e7420696e74657276616c2e64496e697469616c426f6e64734d6f76696e67417665726167651820a0bb0d0000000000047820496e697469616c20626f6e6473206d6f76696e6720617665726167652e4c496e697469616c426f6e647350656e616c74799c080000045c20496e697469616c20626f6e64732070656e616c74792e94496e697469616c546172676574526567697374726174696f6e73506572496e74657276616c9c08020004ac20496e697469616c2074617267657420726567697374726174696f6e732070657220696e74657276616c2e28496e697469616c52686f9c080a0004382052686f20636f6e7374616e742e30496e697469616c4b617070619c08ff7f0440204b6170706120636f6e7374616e742e54496e697469616c4d6178416c6c6f776564556964739c0800100448204d61782055494420636f6e7374616e742e60496e697469616c56616c696461746f725072756e654c656e1820010000000000000004a820496e697469616c2076616c696461746f7220636f6e74657874207072756e696e67206c656e6774682e58496e697469616c5363616c696e674c6177506f7765729c083200046c20496e697469616c207363616c696e67206c617720706f7765722e54496e697469616c496d6d756e697479506572696f649c080010046820496d6d756e69747920506572696f6420436f6e7374616e742e54496e697469616c41637469766974794375746f66669c088813044c20416374697669747920636f6e7374616e742e7c496e697469616c4d6178526567697374726174696f6e73506572426c6f636b9c080100049420496e697469616c206d617820726567697374726174696f6e732070657220626c6f636b2e4c496e697469616c5072756e696e6753636f72659c08ffff049c20496e697469616c207072756e696e672073636f726520666f722065616368206e6575726f6e2e6c496e697469616c4d6178416c6c6f77656456616c696461746f72739c08800004c020496e697469616c206d6178696d756d20616c6c6f7765642076616c696461746f727320706572206e6574776f726b2e68496e697469616c44656661756c7444656c656761746554616b659c08142e048420496e697469616c2064656661756c742064656c65676174696f6e2074616b652e58496e697469616c4d696e44656c656761746554616b659c080000048420496e697469616c206d696e696d756d2064656c65676174696f6e2074616b652e68496e697469616c44656661756c744368696c644b657954616b659c080000047c20496e697469616c2064656661756c74206368696c646b65792074616b652e58496e697469616c4d696e4368696c644b657954616b659c080000047c20496e697469616c206d696e696d756d206368696c646b65792074616b652e58496e697469616c4d61784368696c644b657954616b659c08142e047c20496e697469616c206d6178696d756d206368696c646b65792074616b652e60496e697469616c5765696768747356657273696f6e4b657918200000000000000000047420496e697469616c20776569676874732076657273696f6e206b65792e5c496e697469616c53657276696e67526174654c696d697418203200000000000000047020496e697469616c2073657276696e672072617465206c696d69742e48496e697469616c5478526174654c696d69741820e803000000000000048020496e697469616c207472616e73616374696f6e2072617465206c696d69742e78496e697469616c547844656c656761746554616b65526174654c696d69741820c04b03000000000004b820496e697469616c2064656c65676174652074616b65207472616e73616374696f6e2072617465206c696d69742e78496e697469616c54784368696c644b657954616b65526174654c696d69741820050000000000000004b820496e697469616c206368696c646b65792074616b65207472616e73616374696f6e2072617465206c696d69742e90496e697469616c53656e61746552657175697265645374616b6550657263656e746167651820010000000000000004ec20496e697469616c2070657263656e74616765206f6620746f74616c207374616b6520726571756972656420746f206a6f696e2073656e6174652e58496e697469616c41646a7573746d656e74416c7068611820000000000000000004a820496e697469616c2061646a7573746d656e7420616c706861206f6e206275726e20616e6420706f772e70496e697469616c4e6574776f726b496d6d756e697479506572696f641820e0c4000000000000048020496e697469616c206e6574776f726b20696d6d756e69747920706572696f6470496e697469616c4e6574776f726b4d696e416c6c6f776564556964739c088000049420496e697469616c206d696e696d756d20616c6c6f776564206e6574776f726b205549447364496e697469616c4e6574776f726b4d696e4c6f636b436f737418200010a5d4e8000000048820496e697469616c206e6574776f726b206d696e696d756d206275726e20636f737454496e697469616c5375626e65744f776e65724375749c08142e047020496e697469616c206e6574776f726b207375626e6574206375742e8c496e697469616c4e6574776f726b4c6f636b526564756374696f6e496e74657276616c1820c089010000000000048420496e697469616c206c6f636b20726564756374696f6e20696e74657276616c2e48496e697469616c5375626e65744c696d69749c080c00047020496e697469616c206d617820616c6c6f776564207375626e6574735c496e697469616c4e6574776f726b526174654c696d69741820201c000000000000049020496e697469616c206e6574776f726b206372656174696f6e2072617465206c696d69742c4b657953776170436f7374182000e1f50500000000046c20436f7374206f66207377617070696e67206120686f746b65792e24416c706861486967689c0866e60401012054686520757070657220626f756e6420666f722074686520616c70686120706172616d657465722e205573656420666f72204c697175696420416c7068612e20416c7068614c6f779c0833b304010120546865206c6f77657220626f756e6420666f722074686520616c70686120706172616d657465722e205573656420666f72204c697175696420416c7068612e344c6971756964416c7068614f6e24040004bc204120666c616720746f20696e646963617465206966204c697175696420416c70686120697320656e61626c65642e58496e697469616c4e6574776f726b4d61785374616b651820ffffffffffffffff046c20496e697469616c206e6574776f726b206d6178207374616b652e88496e697469616c436f6c646b6579537761705363686564756c654475726174696f6e1010a08c0000048020436f6c646b65792073776170207363686564756c65206475617274696f6e2e98496e697469616c446973736f6c76654e6574776f726b5363686564756c654475726174696f6e1010a08c0000048c20446973736f6c7665206e6574776f726b207363686564756c65206475726174696f6e40496e697469616c54616f5765696768741820d9cef753e3a59b04045020496e697469616c2054414f207765696768742e010106072c547269756d766972617465012c547269756d766972617465182450726f706f73616c7301000506040004902054686520686173686573206f6620746865206163746976652070726f706f73616c732e2850726f706f73616c4f6600010406341503040004cc2041637475616c2070726f706f73616c20666f72206120676976656e20686173682c20696620697427732063757272656e742e18566f74696e6700010406340906040004b420566f746573206f6e206120676976656e2070726f706f73616c2c206966206974206973206f6e676f696e672e3450726f706f73616c436f756e74010010100000000004482050726f706f73616c7320736f206661722e1c4d656d6265727301005d020400043901205468652063757272656e74206d656d62657273206f662074686520636f6c6c6563746976652e20546869732069732073746f72656420736f7274656420286a7573742062792076616c7565292e145072696d65000000040004650120546865207072696d65206d656d62657220746861742068656c70732064657465726d696e65207468652064656661756c7420766f7465206265686176696f7220696e2063617365206f6620616273656e746174696f6e732e01190301c000010d060848547269756d7669726174654d656d626572730148547269756d7669726174654d656d62657273081c4d656d6265727301001106040004c8205468652063757272656e74206d656d626572736869702c2073746f72656420617320616e206f726465726564205665632e145072696d65000000040004a4205468652063757272656e74207072696d65206d656d6265722c206966206f6e65206578697374732e011d0301c400011506093453656e6174654d656d62657273013453656e6174654d656d62657273081c4d656d6265727301001906040004c8205468652063757272656e74206d656d626572736869702c2073746f72656420617320616e206f726465726564205665632e145072696d65000000040004a4205468652063757272656e74207072696d65206d656d6265722c206966206f6e65206578697374732e01210301c800011d060a1c5574696c6974790001250301cc044c626174636865645f63616c6c735f6c696d69741010aa2a000004a820546865206c696d6974206f6e20746865206e756d626572206f6620626174636865642063616c6c732e0121060b105375646f01105375646f040c4b6579000000040004842054686520604163636f756e74496460206f6620746865207375646f206b65792e013d0301d0000125060c204d756c746973696701204d756c746973696704244d756c746973696773000108050229062d06040004942054686520736574206f66206f70656e206d756c7469736967206f7065726174696f6e732e01410301d40c2c4465706f7369744261736518200029de070000000018590120546865206261736520616d6f756e74206f662063757272656e6379206e656564656420746f207265736572766520666f72206372656174696e672061206d756c746973696720657865637574696f6e206f7220746f842073746f726520612064697370617463682063616c6c20666f72206c617465722e00010120546869732069732068656c6420666f7220616e206164646974696f6e616c2073746f72616765206974656d2077686f73652076616c75652073697a652069733101206034202b2073697a656f662828426c6f636b4e756d6265722c2042616c616e63652c204163636f756e74496429296020627974657320616e642077686f7365206b65792073697a652069738020603332202b2073697a656f66284163636f756e74496429602062797465732e344465706f736974466163746f7218200048e801000000000c55012054686520616d6f756e74206f662063757272656e6379206e65656465642070657220756e6974207468726573686f6c64207768656e206372656174696e672061206d756c746973696720657865637574696f6e2e00250120546869732069732068656c6420666f7220616464696e67203332206279746573206d6f726520696e746f2061207072652d6578697374696e672073746f726167652076616c75652e384d61785369676e61746f7269657310106400000004ec20546865206d6178696d756d20616d6f756e74206f66207369676e61746f7269657320616c6c6f77656420696e20746865206d756c74697369672e0135060d20507265696d6167650120507265696d6167650c24537461747573466f72000104063439060400049020546865207265717565737420737461747573206f66206120676976656e20686173682e4052657175657374537461747573466f72000104063445060400049020546865207265717565737420737461747573206f66206120676976656e20686173682e2c507265696d616765466f72000104065506590604000001490301dc00015d060e245363686564756c657201245363686564756c6572103c496e636f6d706c65746553696e6365000010040000184167656e6461010104051061060400044d01204974656d7320746f2062652065786563757465642c20696e64657865642062792074686520626c6f636b206e756d626572207468617420746865792073686f756c64206265206578656375746564206f6e2e1c5265747269657300010402e47d06040004210120526574727920636f6e66696775726174696f6e7320666f72206974656d7320746f2062652065786563757465642c20696e6465786564206279207461736b20616464726573732e184c6f6f6b75700001040504e4040010f8204c6f6f6b75702066726f6d2061206e616d6520746f2074686520626c6f636b206e756d62657220616e6420696e646578206f6620746865207461736b2e00590120466f72207633202d3e207634207468652070726576696f75736c7920756e626f756e646564206964656e7469746965732061726520426c616b65322d3235362068617368656420746f20666f726d2074686520763430206964656e7469746965732e014d0301e008344d6178696d756d5765696768742c400b0000dd0ee90213cccccccccccccccc04290120546865206d6178696d756d207765696768742074686174206d6179206265207363686564756c65642070657220626c6f636b20666f7220616e7920646973706174636861626c65732e504d61785363686564756c6564506572426c6f636b101032000000141d0120546865206d6178696d756d206e756d626572206f66207363686564756c65642063616c6c7320696e2074686520717565756520666f7220612073696e676c6520626c6f636b2e0018204e4f54453a5101202b20446570656e64656e742070616c6c657473272062656e63686d61726b73206d696768742072657175697265206120686967686572206c696d697420666f72207468652073657474696e672e205365742061c420686967686572206c696d697420756e646572206072756e74696d652d62656e63686d61726b736020666561747572652e0181060f1450726f7879011450726f7879081c50726f7869657301010405008506240000000000000000000845012054686520736574206f66206163636f756e742070726f786965732e204d61707320746865206163636f756e74207768696368206861732064656c65676174656420746f20746865206163636f756e7473210120776869636820617265206265696e672064656c65676174656420746f2c20746f67657468657220776974682074686520616d6f756e742068656c64206f6e206465706f7369742e34416e6e6f756e63656d656e7473010104050095062400000000000000000004ac2054686520616e6e6f756e63656d656e7473206d616465206279207468652070726f787920286b6579292e01550301ec184050726f78794465706f736974426173651820008793030000000010110120546865206261736520616d6f756e74206f662063757272656e6379206e656564656420746f207265736572766520666f72206372656174696e6720612070726f78792e00010120546869732069732068656c6420666f7220616e206164646974696f6e616c2073746f72616765206974656d2077686f73652076616c75652073697a652069732501206073697a656f662842616c616e6365296020627974657320616e642077686f7365206b65792073697a65206973206073697a656f66284163636f756e74496429602062797465732e4850726f78794465706f736974466163746f721820408af7010000000014bc2054686520616d6f756e74206f662063757272656e6379206e6565646564207065722070726f78792061646465642e00350120546869732069732068656c6420666f7220616464696e6720333220627974657320706c757320616e20696e7374616e6365206f66206050726f78795479706560206d6f726520696e746f20616101207072652d6578697374696e672073746f726167652076616c75652e20546875732c207768656e20636f6e6669677572696e67206050726f78794465706f736974466163746f7260206f6e652073686f756c642074616b65f420696e746f206163636f756e7420603332202b2070726f78795f747970652e656e636f646528292e6c656e282960206279746573206f6620646174612e284d617850726f7869657310101400000004f020546865206d6178696d756d20616d6f756e74206f662070726f7869657320616c6c6f77656420666f7220612073696e676c65206163636f756e742e284d617850656e64696e6710104b00000004450120546865206d6178696d756d20616d6f756e74206f662074696d652d64656c6179656420616e6e6f756e63656d656e747320746861742061726520616c6c6f77656420746f2062652070656e64696e672e5c416e6e6f756e63656d656e744465706f736974426173651820005125020000000010310120546865206261736520616d6f756e74206f662063757272656e6379206e656564656420746f207265736572766520666f72206372656174696e6720616e20616e6e6f756e63656d656e742e00490120546869732069732068656c64207768656e2061206e65772073746f72616765206974656d20686f6c64696e672061206042616c616e636560206973206372656174656420287479706963616c6c7920313620206279746573292e64416e6e6f756e63656d656e744465706f736974466163746f72182000990d040000000010d42054686520616d6f756e74206f662063757272656e6379206e65656465642070657220616e6e6f756e63656d656e74206d6164652e00590120546869732069732068656c6420666f7220616464696e6720616e20604163636f756e744964602c2060486173686020616e642060426c6f636b4e756d6265726020287479706963616c6c79203638206279746573298c20696e746f2061207072652d6578697374696e672073746f726167652076616c75652e01a506102052656769737472790120526567697374727904284964656e746974794f660001040500a90604000464204964656e746974792064617461206279206163636f756e74015d0301f40c4c4d61784164646974696f6e616c4669656c6473101001000000085420436f6e66696775726174696f6e206669656c6473a8204d6178696d756d20757365722d636f6e66696775726564206164646974696f6e616c206669656c647338496e697469616c4465706f736974182000e1f5050000000004d42054686520616d6f756e742068656c64206f6e206465706f73697420666f7220612072656769737465726564206964656e74697479304669656c644465706f736974182000e1f50500000000042d012054686520616d6f756e742068656c64206f6e206465706f73697420706572206164646974696f6e616c206669656c6420666f7220612072656769737465726564206964656e746974792e01ad06112c436f6d6d69746d656e7473012c436f6d6d69746d656e74730c24526174654c696d69740100101064000000047c205468652072617465206c696d697420666f7220636f6d6d69746d656e747330436f6d6d69746d656e744f6600010806057502b10604000464204964656e746974792064617461206279206163636f756e74384c617374436f6d6d69746d656e74000108060575021004000001690401f810244d61784669656c647310100100000004290120546865206d6178696d756d206e756d626572206f66206164646974696f6e616c206669656c647320746861742063616e20626520616464656420746f206120636f6d6d69746d656e7438496e697469616c4465706f7369741820000000000000000004d42054686520616d6f756e742068656c64206f6e206465706f73697420666f7220612072656769737465726564206964656e74697479304669656c644465706f73697418200000000000000000042d012054686520616d6f756e742068656c64206f6e206465706f73697420706572206164646974696f6e616c206669656c6420666f7220612072656769737465726564206964656e746974792e4044656661756c74526174654c696d6974101064000000047c205468652072617465206c696d697420666f7220636f6d6d69746d656e747301b506122841646d696e5574696c7300017d0501fc0001b9061320536166654d6f64650120536166654d6f64650830456e7465726564556e74696c000010040014290120436f6e7461696e7320746865206c61737420626c6f636b206e756d62657220746861742074686520736166652d6d6f64652077696c6c2072656d61696e20656e746572656420696e2e00a4202053657420746f20604e6f6e6560207768656e20736166652d6d6f6465206973206578697465642e00510120536166652d6d6f6465206973206175746f6d61746963616c6c7920657869746564207768656e207468652063757272656e7420626c6f636b206e756d626572206578636565647320746869732076616c75652e204465706f736974730001080505bd0618040010350120486f6c64732074686520726573657276652074686174207761732074616b656e2066726f6d20616e206163636f756e74206174206120737065636966696320626c6f636b206e756d6265722e00750120546869732068656c707320676f7665726e616e636520746f206861766520616e206f76657276696577206f66206f75747374616e64696e67206465706f7369747320746861742073686f756c642062652072657475726e6564206f722420736c61736865642e0181050101011434456e7465724475726174696f6e10100000000004210120466f7220686f77206d616e7920626c6f636b732074686520736166652d6d6f64652077696c6c20626520656e7465726564206279205b6050616c6c65743a3a656e746572605d2e38457874656e644475726174696f6e1010000000000c4d0120466f7220686f77206d616e7920626c6f636b732074686520736166652d6d6f64652063616e20626520657874656e6465642062792065616368205b6050616c6c65743a3a657874656e64605d2063616c6c2e004d01205468697320646f6573206e6f7420696d706f736520612068617264206c696d69742061732074686520736166652d6d6f64652063616e20626520657874656e646564206d756c7469706c652074696d65732e48456e7465724465706f736974416d6f756e74c10604000c05012054686520616d6f756e7420746861742077696c6c2062652072657365727665642075706f6e2063616c6c696e67205b6050616c6c65743a3a656e746572605d2e00410120604e6f6e656020646973616c6c6f7773207065726d697373696f6e6c6573736c7920656e61626c696e672074686520736166652d6d6f646520616e6420697320612073616e652064656661756c742e4c457874656e644465706f736974416d6f756e74c10604000c09012054686520616d6f756e7420746861742077696c6c2062652072657365727665642075706f6e2063616c6c696e67205b6050616c6c65743a3a657874656e64605d2e00450120604e6f6e656020646973616c6c6f7773207065726d697373696f6e6c6573736c7920657874656e64696e672074686520736166652d6d6f646520616e6420697320612073616e652064656661756c742e3052656c6561736544656c6179d101040020490120546865206d696e696d616c206475726174696f6e2061206465706f7369742077696c6c2072656d61696e20726573657276656420616674657220736166652d6d6f646520697320656e7465726564206f72490120657874656e6465642c20756e6c657373205b6050616c6c65743a3a666f7263655f72656c656173655f6465706f736974605d206973207375636365737366756c6c792063616c6c656420736f6f6e65722e005901204576657279206465706f736974206973207469656420746f20612073706563696669632061637469766174696f6e206f7220657874656e73696f6e2c20746875732065616368206465706f7369742063616e206265e82072656c656173656420696e646570656e64656e746c79206166746572207468652064656c617920666f7220697420686173207061737365642e00450120604e6f6e656020646973616c6c6f7773207065726d697373696f6e6c6573736c792072656c656173696e672074686520736166652d6d6f6465206465706f7369747320616e6420697320612073616e65242064656661756c742e01c5061420457468657265756d0120457468657265756d141c50656e64696e670100c906040004d02043757272656e74206275696c64696e6720626c6f636b2773207472616e73616374696f6e7320616e642072656365697074732e3043757272656e74426c6f636b0000ed0604000470205468652063757272656e7420457468657265756d20626c6f636b2e3c43757272656e745265636569707473000001070400047c205468652063757272656e7420457468657265756d2072656365697074732e6843757272656e745472616e73616374696f6e53746174757365730000050704000488205468652063757272656e74207472616e73616374696f6e2073746174757365732e24426c6f636b48617368010104054101348000000000000000000000000000000000000000000000000000000000000000000001850501090100010907150c45564d010c45564d18304163636f756e74436f646573010104020d0138040000504163636f756e74436f6465734d65746164617461000104020d010d070400003c4163636f756e7453746f7261676573010108020211073480000000000000000000000000000000000000000000000000000000000000000000205375696369646564000104020d01a40400004c57686974656c697374656443726561746f72730100bd050400005444697361626c6557686974656c697374436865636b01002404000001ad0501350100011507162845564d436861696e4964012845564d436861696e4964041c436861696e49640100182000000000000000000448205468652045564d20636861696e2049442e00000000172844796e616d6963466565012844796e616d6963466565082c4d696e47617350726963650100410180000000000000000000000000000000000000000000000000000000000000000000445461726765744d696e47617350726963650000410104000001c105000000181c42617365466565011c42617365466565083442617365466565506572476173010041018000c817a8040000000000000000000000000000000000000000000000000000000028456c6173746963697479010049011048e801000001c505013d01000019144472616e6401144472616e641030426561636f6e436f6e6669670100f1053903810183cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a030000002721e6648052db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e97180f477d5c89f21a17c863a7f937c6a6d15859414d2be09cd448d4279af331c5d3e60626c732d756e636861696e65642d67312d7266633933383020717569636b6e6574047c20746865206472616e6420626561636f6e20636f6e66696775726174696f6e1850756c7365730001040218d90504000468206d617020726f756e64206e756d62657220746f2070756c73653c4c61737453746f726564526f756e6401001820000000000000000000384e657874556e7369676e656441740100101000000000140d0120446566696e65732074686520626c6f636b207768656e206e65787420756e7369676e6564207472616e73616374696f6e2077696c6c2062652061636365707465642e001d0120546f2070726576656e74207370616d206f6620756e7369676e65642028616e6420756e706169642129207472616e73616374696f6e73206f6e20746865206e6574776f726b2ca4207765206f6e6c7920616c6c6f77206f6e65207472616e73616374696f6e2070657220626c6f636b2e250120546869732073746f7261676520656e74727920646566696e6573207768656e206e6577207472616e73616374696f6e20697320676f696e6720746f2062652061636365707465642e01c905014d010840556e7369676e65645072696f726974791820000010000000000010f0204120636f6e66696775726174696f6e20666f722062617365207072696f72697479206f6620756e7369676e6564207472616e73616374696f6e732e0015012054686973206973206578706f73656420736f20746861742069742063616e2062652074756e656420666f7220706172746963756c61722072756e74696d652c207768656eb4206d756c7469706c652070616c6c6574732073656e6420756e7369676e6564207472616e73616374696f6e732e4048747470466574636854696d656f75741820e80300000000000008490120546865206d6178696d756d206e756d626572206f66206d696c6c697365636f6e6473207765206172652077696c6c696e6720746f207761697420666f72207468652048545450207265717565737420746f2820636f6d706c6574652e0119071a1d07042c48436865636b4e6f6e5a65726f53656e6465722507a440436865636b5370656356657273696f6e29071038436865636b547856657273696f6e2d071030436865636b47656e6573697331073438436865636b4d6f7274616c69747935073428436865636b4e6f6e63653d07a42c436865636b5765696768744107a4604368617267655472616e73616374696f6e5061796d656e744507a46053756274656e736f725369676e6564457874656e73696f6e4907a468436f6d6d69746d656e74735369676e6564457874656e73696f6e5107a444436865636b4d65746164617461486173685507e84d07 \ No newline at end of file diff --git a/tests/unit_tests/bittensor_tests/utils/__init__.py b/tests/integration_tests/__init__.py similarity index 100% rename from tests/unit_tests/bittensor_tests/utils/__init__.py rename to tests/integration_tests/__init__.py diff --git a/tests/integration_tests/cpu_tests/bittensor/test_mnist_node.py b/tests/integration_tests/cpu_tests/bittensor/test_mnist_node.py deleted file mode 100644 index 0a889d5993..0000000000 --- a/tests/integration_tests/cpu_tests/bittensor/test_mnist_node.py +++ /dev/null @@ -1,201 +0,0 @@ -#!/bin/python3 -"""Training a MNIST Neuron. -This file demonstrates a training pipeline for an MNIST Neuron. -Example: - $ python neurons/mnist.py -""" -import argparse -import math -import os -import time -import torch -from termcolor import colored -import torch.nn.functional as F -import torch.optim as optim -import torchvision -import torchvision.transforms as transforms -from torch.utils.tensorboard import SummaryWriter - -from munch import Munch -from loguru import logger - -import bittensor -from bittensor import Session -from bittensor.utils.logging import log_all -from bittensor.subtensor.interface import Keypair -from bittensor.config import Config -from bittensor.synapse import Synapse -from bittensor.synapses.ffnn import FFNNSynapse - -def add_args(parser: argparse.ArgumentParser): - parser.add_argument('--neuron.datapath', default='data/', type=str, help='Path to load and save data.') - parser.add_argument('--neuron.n_epochs', default=1, type=int, help='Number of training epochs.') - parser.add_argument('--neuron.learning_rate', default=0.01, type=float, help='Training initial learning rate.') - parser.add_argument('--neuron.momentum', default=0.9, type=float, help='Training initial momentum for SGD.') - parser.add_argument('--neuron.batch_size_train', default=64, type=int, help='Training batch size.') - parser.add_argument('--neuron.batch_size_test', default=64, type=int, help='Testing batch size.') - parser.add_argument('--neuron.log_interval', default=150, type=int, help='Batches until neuron prints log statements.') - parser.add_argument('--neuron.sync_interval', default=150, type=int, help='Batches before we we sync with chain and emit new weights.') - parser.add_argument('--neuron.name', default='mnist', type=str, help='Trials for this neuron go in neuron.datapath / neuron.name') - parser.add_argument('--neuron.trial_id', default=str(time.time()).split('.')[0], type=str, help='Saved models go in neuron.datapath / neuron.name / neuron.trial_id') - FFNNSynapse.add_args(parser) - -def check_config(config: Munch): - assert config.neuron.log_interval > 0, "log_interval dimension must positive" - assert config.neuron.momentum > 0 and config.neuron.momentum < 1, "momentum must be a value between 0 and 1" - assert config.neuron.batch_size_train > 0, "batch_size_train must a positive value" - assert config.neuron.batch_size_test > 0, "batch_size_test must a positive value" - assert config.neuron.learning_rate > 0, "learning rate must be a positive value." - trial_path = '{}/{}/{}'.format(config.neuron.datapath, config.neuron.name, config.neuron.trial_id) - config.neuron.trial_path = trial_path - if not os.path.exists(config.neuron.trial_path): - os.makedirs(config.neuron.trial_path) - FFNNSynapse.check_config(config) - -def start(config, session): - # ---- Model ---- - model = FFNNSynapse(config, session) # Feedforward neural network with PKMDendrite. - device = torch.device("cuda" if torch.cuda.is_available() else "cpu") - model.to( device ) # Set model to device - - # ---- Optimizer ---- - optimizer = optim.SGD(model.parameters(), lr=config.neuron.learning_rate, momentum=config.neuron.momentum) - scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=10.0, gamma=0.1) - - # ---- Dataset ---- - train_data = torchvision.datasets.MNIST(root = config.neuron.datapath + "datasets/", train=True, download=True, transform=transforms.ToTensor()) - trainloader = torch.utils.data.DataLoader(train_data, batch_size = config.neuron.batch_size_train, shuffle=True, num_workers=2) - test_data = torchvision.datasets.MNIST(root = config.neuron.datapath + "datasets/", train=False, download=True, transform=transforms.ToTensor()) - testloader = torch.utils.data.DataLoader(test_data, batch_size = config.neuron.batch_size_test, shuffle=False, num_workers=2) - - # ---- Tensorboard ---- - global_step = 0 - tensorboard = SummaryWriter(log_dir = config.neuron.trial_path) - - # --- Test epoch ---- - def test (): - model.eval() # Turns off Dropoutlayers, BatchNorm etc. - with torch.no_grad(): # Turns off gradient computation for inference speed up. - loss = 0.0; accuracy = 0.0 - for _, (images, labels) in enumerate(testloader): - # ---- Forward pass ---- - outputs = model.forward( - images = images.to(model.device), - targets = torch.LongTensor(labels).to(model.device), - remote = False # *without* rpc-queries being made. - ) - loss = loss + outputs.loss / len(testloader) - accuracy = outputs.metadata['local_accuracy'] / len(testloader) - return loss, accuracy - - # ---- Train epoch ---- - def train(epoch: int, global_step: int): - - # ---- Init training state ---- - model.train() # Turn on dropout etc. - session.metagraph.sync() # Sync with the chain. - row_weights = session.metagraph.W[ 0, :] # My weights on the chain-state (zeros initially). - - history = [] - for batch_idx, (images, targets) in enumerate(trainloader): - global_step += 1 - - # ---- Forward pass ---- - output = model( - images = images.to(model.device), - targets = torch.LongTensor(targets).to(model.device), - remote = True # *with* rpc-queries made to the network. - ) - - # ---- Backward pass ---- - output.loss.backward() # Accumulates gradients on the model. - optimizer.step() # Applies accumulated gradients. - optimizer.zero_grad() # Zeros out gradients for next accummulation - - # ---- Serve Model ---- - session.serve( model.deepcopy() ) # Serve the newest model. - - # ---- Step Logs + Tensorboard ---- - history.append(output) # Save for later analysis/logs. - processed = ((batch_idx + 1) * config.neuron.batch_size_train) - progress = (100. * processed) / len(train_data) - logger.info('GS: {} Epoch: {} [{}/{} ({})]\t Loss: {}\t Acc: {}', - colored('{}'.format(global_step), 'blue'), - colored('{}'.format(epoch), 'blue'), - colored('{}'.format(processed), 'green'), - colored('{}'.format(len(train_data)), 'red'), - colored('{:.2f}%'.format(progress), 'green'), - colored('{:.4f}'.format(output.local_target_loss.item()), 'green'), - colored('{:.4f}'.format(output.metadata['local_accuracy'].item()), 'green')) - tensorboard.add_scalar('Rloss', output.remote_target_loss.item(), global_step) - tensorboard.add_scalar('Lloss', output.local_target_loss.item(), global_step) - tensorboard.add_scalar('Dloss', output.distillation_loss.item(), global_step) - if (batch_idx+1) % config.neuron.log_interval == 0: - log_all(session, history); history = [] # Log batch history. - - # ---- Update State ---- - batch_weights = torch.mean(output.weights, axis = 0) # Average over batch. - row_weights = (1 - 0.03) * row_weights + 0.03 * batch_weights # Moving avg update. - row_weights = F.normalize(row_weights, p = 1, dim = 0) # Ensure normalization. - - if (batch_idx+1) % config.neuron.sync_interval == 0: - # ---- Sync Metagraph State ---- - logger.info('Emitting with weights {}', row_weights.tolist()) - session.metagraph.emit( row_weights, wait_for_inclusion = False) # Sets my row-weights on the chain. - session.metagraph.sync() # Pulls the latest metagraph state (with my update.) - row_weights = session.metagraph.W[ 0, :] - - # ---- Update Axon Priority ---- - col_weights = session.metagraph.W[:,0] # weights to me. - session.axon.set_priority( session.metagraph.neurons, col_weights ) # Sets the nucleus-backend request priority. - - break - - # ---- Loop epochs ---- - best_test_loss = math.inf; - best_test_accuracy = -math.inf - start_time = time.time() - for epoch in range(config.neuron.n_epochs): - - # ---- Train model ---- - train(epoch, global_step) - scheduler.step() - - # ---- Test model ---- - test_loss, test_accuracy = test() - best_test_loss = min(test_loss, best_test_loss) - best_test_accuracy = max(test_accuracy, best_test_accuracy) - - # ---- Save Best ---- - if test_loss < best_test_loss: - best_test_loss = test_loss # Update best loss. - logger.info( 'Saving/Serving model: epoch: {}, accuracy: {}, loss: {}, path: {}/model.torch'.format(epoch, test_accuracy, best_test_loss, config.neuron.trial_path)) - torch.save( {'epoch': epoch, 'model': model.state_dict(), 'loss': best_test_loss},"{}/model.torch".format(config.neuron.trial_path)) - tensorboard.add_scalar('Test loss', test_loss, global_step) - - # Test checks. - time_elapsed = time.time() - start_time - logger.info("Total time elapsed: {}".format(time_elapsed)) - assert best_test_loss <= 0.1 - assert best_test_accuracy > 0.80 - assert len(session.metagraph.state.neurons.tolist()) > 0 - assert time_elapsed < (300 * config.neuron.n_epochs) # 1 epoch of MNIST should take less than 5 mins. - -def main(): - - # ---- Load bittensor config ---- - parser = argparse.ArgumentParser(); add_args(parser) - config = Config.load(parser); check_config(config) - logger.info(Config.toString(config)) - - # ---- Load Session ---- - session = bittensor.init(config) - - # ---- Start Neuron ---- - with session: - start(config, session) - -if __name__ == "__main__": - main() - - diff --git a/tests/integration_tests/cpu_tests/bittensor/test_server.py b/tests/integration_tests/cpu_tests/bittensor/test_server.py deleted file mode 100644 index b90fb0332c..0000000000 --- a/tests/integration_tests/cpu_tests/bittensor/test_server.py +++ /dev/null @@ -1,51 +0,0 @@ -from bittensor import bittensor_pb2_grpc as proto_grpc -from bittensor import bittensor_pb2 as proto_pb2 -from concurrent import futures - -import grpc - - -class Axon(proto_grpc.BittensorServicer): - - def __init__(self): - pass - - def Forward(self, context, request): - response = proto_pb2.TensorMessage() - return response - - def Backward(self, contect, request): - response = proto_pb2.TensorMessage() - return response - - -def create_server(): - address = "[::]:8812" - server = grpc.server(futures.ThreadPoolExecutor(max_workers=10)) - axon = Axon() - proto_grpc.add_BittensorServicer_to_server(axon, server) - server.add_insecure_port(address) - return server - - -def test_create(): - server = create_server() - server.start() - server.stop(0) - - -def test_client(): - - server = create_server() - server.start() - - address = "localhost:8812" - channel = grpc.insecure_channel(address) - stub = proto_grpc.BittensorStub(channel) - - request = proto_pb2.TensorMessage() - response = stub.Forward(request) - - request = proto_pb2.TensorMessage() - response = stub.Backward(request) - server.stop(0) diff --git a/tests/integration_tests/gpu_tests/bittensor/cuda_test.py b/tests/integration_tests/gpu_tests/bittensor/cuda_test.py deleted file mode 100644 index 03fdf17300..0000000000 --- a/tests/integration_tests/gpu_tests/bittensor/cuda_test.py +++ /dev/null @@ -1,11 +0,0 @@ -import unittest -import torch - -class TestCUDA(unittest.TestCase): - device = None - - def setUp(self): - self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu") - - def test_cuda(self): - assert self.device == torch.device("cuda") \ No newline at end of file diff --git a/tests/integration_tests/subtensor_client_tests/test_connect.py b/tests/integration_tests/subtensor_client_tests/test_connect.py deleted file mode 100644 index fcc1ee9ba8..0000000000 --- a/tests/integration_tests/subtensor_client_tests/test_connect.py +++ /dev/null @@ -1,29 +0,0 @@ -from bittensor.subtensor.client import WSClient -from bittensor.subtensor.interface import Keypair -from loguru import logger -import pytest - -logger.remove() # Shut up loguru - - - - -@pytest.mark.asyncio -async def test_connect_success(): - socket = "localhost:9944" - keypair = Keypair.create_from_uri('//Alice') - client = WSClient(socket, keypair) - - client.connect() - result = await client.is_connected() - assert result == True - -@pytest.mark.asyncio -async def test_connect_failed(): - socket = 'localhost:9999' - keypair = Keypair.create_from_uri('//Alice') - client = WSClient(socket, keypair) - - client.connect() - result = await client.is_connected() - assert result == False diff --git a/tests/integration_tests/subtensor_client_tests/test_subscribe.py b/tests/integration_tests/subtensor_client_tests/test_subscribe.py deleted file mode 100644 index 2b3a9336a6..0000000000 --- a/tests/integration_tests/subtensor_client_tests/test_subscribe.py +++ /dev/null @@ -1,22 +0,0 @@ -from bittensor.subtensor.client import WSClient -from bittensor.subtensor.interface import Keypair -from loguru import logger -import pytest - -logger.remove() # Shut up loguru - - -socket = "localhost:9944" -keypair = Keypair.create_from_uri('//Alice') -client = WSClient(socket, keypair) - - -@pytest.mark.asyncio -async def test_subscribe(): - client.connect() - await client.is_connected() - - await client.subscribe("127.0.0.1", 666) - - - diff --git a/tests/integration_tests/test_metagraph_integration.py b/tests/integration_tests/test_metagraph_integration.py new file mode 100644 index 0000000000..005b1ae0c3 --- /dev/null +++ b/tests/integration_tests/test_metagraph_integration.py @@ -0,0 +1,116 @@ +import os +from unittest import mock + +import torch + +import bittensor +from bittensor.core.metagraph import METAGRAPH_STATE_DICT_NDARRAY_KEYS, get_save_dir +from bittensor.utils.mock import MockSubtensor + +_subtensor_mock: MockSubtensor = MockSubtensor() + + +def setUpModule(): + _subtensor_mock.reset() + _subtensor_mock.create_subnet(netuid=3) + _subtensor_mock.set_difficulty(netuid=3, difficulty=0) # Set diff 0 + + +class TestMetagraph: + def setup_method(self): + self.sub = MockSubtensor() + self.metagraph = bittensor.Metagraph(netuid=3, network="mock", sync=False) + + def test_print_empty(self): + print(self.metagraph) + + def test_lite_sync(self): + with mock.patch.object( + self.sub, "get_metagraph_info", return_value=mock.MagicMock() + ): + self.metagraph.sync(lite=True, subtensor=self.sub) + + def test_full_sync(self): + with mock.patch.object( + self.sub, "get_metagraph_info", return_value=mock.MagicMock() + ): + self.metagraph.sync(lite=False, subtensor=self.sub) + + def test_sync_block_0(self): + with mock.patch.object( + self.sub, "get_metagraph_info", return_value=mock.MagicMock() + ): + self.metagraph.sync(lite=True, block=0, subtensor=self.sub) + + def test_load_sync_save(self): + with ( + mock.patch.object(self.sub, "neurons_lite", return_value=[]), + mock.patch.object( + self.sub, "get_metagraph_info", return_value=mock.MagicMock() + ), + ): + self.metagraph.sync(lite=True, subtensor=self.sub) + self.metagraph.save() + self.metagraph.load() + self.metagraph.save() + + def test_load_sync_save_from_torch(self): + with ( + mock.patch.object(self.sub, "neurons_lite", return_value=[]), + mock.patch.object( + self.sub, "get_metagraph_info", return_value=mock.MagicMock() + ), + ): + self.metagraph.sync(lite=True, subtensor=self.sub) + + def deprecated_save_torch(metagraph): + save_directory = get_save_dir(metagraph.network, metagraph.netuid) + os.makedirs(save_directory, exist_ok=True) + graph_filename = save_directory + f"/block-{metagraph.block.item()}.pt" + state_dict = metagraph.state_dict() + for key in METAGRAPH_STATE_DICT_NDARRAY_KEYS: + state_dict[key] = torch.nn.Parameter( + torch.tensor(state_dict[key]), requires_grad=False + ) + torch.save(state_dict, graph_filename) + + deprecated_save_torch(self.metagraph) + self.metagraph.load() + + def test_state_dict(self): + self.metagraph.load() + state = self.metagraph.state_dict() + assert "version" in state + assert "n" in state + assert "block" in state + assert "stake" in state + assert "ranks" in state + assert "trust" in state + assert "consensus" in state + assert "validator_trust" in state + assert "incentive" in state + assert "emission" in state + assert "dividends" in state + assert "active" in state + assert "last_update" in state + assert "validator_permit" in state + assert "weights" in state + assert "bonds" in state + assert "uids" in state + + def test_properties(self): + metagraph = self.metagraph + metagraph.hotkeys + metagraph.coldkeys + metagraph.addresses + metagraph.validator_trust + metagraph.S + metagraph.R + metagraph.I + metagraph.E + metagraph.C + metagraph.T + metagraph.Tv + metagraph.D + metagraph.B + metagraph.W diff --git a/tests/integration_tests/test_subtensor_integration.py b/tests/integration_tests/test_subtensor_integration.py new file mode 100644 index 0000000000..b738d06808 --- /dev/null +++ b/tests/integration_tests/test_subtensor_integration.py @@ -0,0 +1,164 @@ +import pytest + +from bittensor.core.chain_data import AxonInfo, NeuronInfo +from bittensor.core.subtensor import Subtensor +from bittensor.utils.balance import Balance +from tests.helpers.helpers import FakeWebsocket +from bittensor.utils.mock.subtensor_mock import MockSubtensor + + +@pytest.fixture +def hotkey(): + yield "5DkzsviNQr4ZePXMmEfNPDcE7cQ9cVyepmQbgUw6YT3odcwh" + + +@pytest.fixture +def netuid(): + yield 23 + + +async def prepare_test(mocker, seed, **subtensor_args): + """ + Helper function: sets up the test environment. + """ + mocker.patch( + "async_substrate_interface.sync_substrate.connect", + mocker.Mock(return_value=FakeWebsocket(seed=seed)), + ) + subtensor = Subtensor("unknown", _mock=True, **subtensor_args) + return subtensor + + +# TODO: Improve integration tests workflow (https://github.com/opentensor/bittensor/issues/2435#issuecomment-2825858004) +# @pytest.mark.asyncio +# async def test_get_all_subnets_info(mocker): +# subtensor = await prepare_test(mocker, "get_all_subnets_info") +# result = subtensor.get_all_subnets_info() +# assert isinstance(result, list) +# assert result[0].owner_ss58 == "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM" +# assert result[1].kappa == 32767 +# assert result[1].max_weight_limit == 65535 +# assert result[1].blocks_since_epoch == 88 + + +# TODO: Improve integration tests workflow (https://github.com/opentensor/bittensor/issues/2435#issuecomment-2825858004) +# @pytest.mark.asyncio +# async def test_metagraph(mocker): +# subtensor = await prepare_test(mocker, "metagraph") +# result = subtensor.metagraph(1) +# assert result.n == 1 +# assert result.netuid == 1 +# assert result.block == 3264143 + + +@pytest.mark.asyncio +async def test_get_netuids_for_hotkey(mocker): + subtensor = await prepare_test(mocker, "get_netuids_for_hotkey") + result = subtensor.get_netuids_for_hotkey( + "5DkzsviNQr4ZePXMmEfNPDcE7cQ9cVyepmQbgUw6YT3odcwh" + ) + assert result == [23] + + +@pytest.mark.asyncio +async def test_get_current_block(mocker): + subtensor = await prepare_test(mocker, "get_current_block") + result = subtensor.get_current_block() + assert result == 3264143 + + +@pytest.mark.asyncio +async def test_is_hotkey_registered_any(mocker): + subtensor = await prepare_test(mocker, "is_hotkey_registered_any") + result = subtensor.is_hotkey_registered_any( + "5DkzsviNQr4ZePXMmEfNPDcE7cQ9cVyepmQbgUw6YT3odcwh" + ) + assert result is True + + +@pytest.mark.asyncio +async def test_is_hotkey_registered_on_subnet(mocker): + subtensor = await prepare_test(mocker, "is_hotkey_registered_on_subnet") + result = subtensor.is_hotkey_registered_on_subnet( + "5DkzsviNQr4ZePXMmEfNPDcE7cQ9cVyepmQbgUw6YT3odcwh", 23 + ) + assert result is True + + +@pytest.mark.asyncio +async def test_is_hotkey_registered(mocker): + subtensor = await prepare_test(mocker, "is_hotkey_registered") + result = subtensor.is_hotkey_registered( + "5DkzsviNQr4ZePXMmEfNPDcE7cQ9cVyepmQbgUw6YT3odcwh" + ) + assert result is True + + +@pytest.mark.asyncio +async def test_blocks_since_last_update(mocker): + subtensor = await prepare_test(mocker, "blocks_since_last_update") + result = subtensor.blocks_since_last_update(1, 0) + assert result == 3264146 + + +@pytest.mark.asyncio +async def test_get_block_hash(mocker): + subtensor = await prepare_test(mocker, "get_block_hash") + result = subtensor.get_block_hash(3234677) + assert ( + result == "0xe89482ae7892ab5633f294179245f4058a99781e15f21da31eb625169da5d409" + ) + + +@pytest.mark.asyncio +async def test_subnetwork_n(mocker): + subtensor = await prepare_test(mocker, "subnetwork_n") + result = subtensor.subnetwork_n(1) + assert result == 94 + + +@pytest.mark.asyncio +async def test_get_neuron_for_pubkey_and_subnet(mocker): + subtensor = await prepare_test(mocker, "get_neuron_for_pubkey_and_subnet") + result = subtensor.get_neuron_for_pubkey_and_subnet( + "5EU2xVWC7qffsUNGtvakp5WCj7WGJMPkwG1dsm3qnU2Kqvee", 1 + ) + assert isinstance(result, NeuronInfo) + assert result.hotkey == "5EU2xVWC7qffsUNGtvakp5WCj7WGJMPkwG1dsm3qnU2Kqvee" + assert isinstance(result.total_stake, Balance) + assert isinstance(result.axon_info, AxonInfo) + assert result.is_null is False + + +def test_mock_subtensor_force_register_neuron(): + """Tests the force_register_neuron method of the MockSubtensor class.""" + # Preps + test_netuid = 1 + subtensor = MockSubtensor() + subtensor.create_subnet(netuid=test_netuid) + + uid1 = subtensor.force_register_neuron(test_netuid, "hk1", "cc1") + uid2 = subtensor.force_register_neuron(test_netuid, "hk2", "cc2") + + # Calls + neurons = subtensor.neurons(test_netuid) + neuron1 = subtensor.neuron_for_uid(uid1, test_netuid) + neuron2 = subtensor.neuron_for_uid(uid2, test_netuid) + + # Assertions + assert len(neurons) == 2 + assert [neuron1, neuron2] == neurons + assert neuron1.hotkey == "hk1" + assert neuron1.coldkey == "cc1" + assert neuron2.hotkey == "hk2" + assert neuron2.coldkey == "cc2" + + +@pytest.mark.asyncio +async def test_archive_node_retry(mocker): + subtensor = await prepare_test( + mocker, "retry_archive", archive_endpoints=["ws://fake-endpoi.nt"] + ) + current_block = subtensor.substrate.get_block_number() + old_block = current_block - 1000 + assert isinstance((subtensor.substrate.get_block(block_number=old_block)), dict) diff --git a/tests/integration_tests/test_timelock.py b/tests/integration_tests/test_timelock.py new file mode 100644 index 0000000000..33e31db782 --- /dev/null +++ b/tests/integration_tests/test_timelock.py @@ -0,0 +1,83 @@ +import struct +import time + +import pytest + +from bittensor.core import timelock + + +def test_encrypt_returns_valid_tuple(): + """Test that encrypt() returns a (bytes, int) tuple.""" + encrypted, reveal_round = timelock.encrypt("Bittensor", n_blocks=1) + assert isinstance(encrypted, bytes) + assert isinstance(reveal_round, int) + assert reveal_round > 0 + + +def test_encrypt_with_fast_block_time(): + """Test encrypt() with fast-blocks mode (block_time = 0.25s).""" + encrypted, reveal_round = timelock.encrypt("Fast mode", 5, block_time=0.25) + assert isinstance(encrypted, bytes) + assert isinstance(reveal_round, int) + + +def test_decrypt_returns_bytes_or_none(): + """Test that decrypt() returns bytes after reveal round, or None before.""" + data = b"Decode me" + encrypted, reveal_round = timelock.encrypt(data, 1) + + current_round = timelock.get_latest_round() + if current_round < reveal_round: + decrypted = timelock.decrypt(encrypted) + assert decrypted is None + else: + decrypted = timelock.decrypt(encrypted) + assert decrypted == data + + +def test_decrypt_raises_if_no_errors_false_and_invalid_data(): + """Test that decrypt() raises an error on invalid data when no_errors=False.""" + with pytest.raises(Exception): + timelock.decrypt(b"corrupt data", no_errors=False) + + +def test_decrypt_with_return_str(): + """Test decrypt() with return_str=True returns a string.""" + plaintext = "Stringified!" + encrypted, _ = timelock.encrypt(plaintext, 1, block_time=0.25) + result = timelock.decrypt(encrypted, no_errors=True, return_str=True) + if result is not None: + assert isinstance(result, str) + + +def test_get_latest_round_is_monotonic(): + """Test that get_latest_round() is monotonic over time.""" + r1 = timelock.get_latest_round() + time.sleep(3) + r2 = timelock.get_latest_round() + assert r2 >= r1 + + +def test_wait_reveal_and_decrypt_auto_round(): + """Test wait_reveal_and_decrypt() without explicit reveal_round.""" + msg = "Reveal and decrypt test" + encrypted, _ = timelock.encrypt(msg, 1) + result = timelock.wait_reveal_and_decrypt(encrypted, return_str=True) + assert result == msg + + +def test_wait_reveal_and_decrypt_manual_round(): + """Test wait_reveal_and_decrypt() with explicit reveal_round.""" + msg = "Manual round decryption" + encrypted, reveal_round = timelock.encrypt(msg, 1) + result = timelock.wait_reveal_and_decrypt(encrypted, reveal_round, return_str=True) + assert result == msg + + +def test_unpack_reveal_round_struct(): + """Test that reveal_round can be extracted from encrypted data.""" + encrypted, reveal_round = timelock.encrypt("parse test", 1) + parsed = struct.unpack( + "', '0x08'), 2) - - async def test_encode_scale(self): - self.assertEqual(self.substrate.encode_scale('Compact', 3), '0x0c') - - @pytest.mark.asyncio - async def test_get_type_definition(self): - self.assertDictEqual(await self.substrate.get_type_definition('Bytes'), { - 'decoder_class': 'Bytes', - 'is_primitive_core': False, - 'is_primitive_runtime': True, - 'spec_version': 2023, - 'type_string': 'Bytes'} - ) - - @pytest.mark.asyncio - async def test_get_metadata_modules(self): - for module in await self.substrate.get_metadata_modules(): - self.assertIn('module_id', module) - self.assertIn('name', module) - self.assertEqual(module['spec_version'], 2023) - - @pytest.mark.asyncio - async def test_get_metadata_call_function(self): - call_function = await self.substrate.get_metadata_call_function("Balances", "transfer") - self.assertEqual(call_function['module_name'], "Balances") - self.assertEqual(call_function['call_name'], "transfer") - self.assertEqual(call_function['spec_version'], 2023) - - @pytest.mark.asyncio - async def test_get_metadata_event(self): - event = await self.substrate.get_metadata_event("Balances", "Transfer") - self.assertEqual(event['module_name'], "Balances") - self.assertEqual(event['event_name'], "Transfer") - self.assertEqual(event['spec_version'], 2023) - - @pytest.mark.asyncio - async def test_get_metadata_constant(self): - constant = await self.substrate.get_metadata_constant("System", "BlockHashCount") - self.assertEqual(constant['module_name'], "System") - self.assertEqual(constant['constant_name'], "BlockHashCount") - self.assertEqual(constant['spec_version'], 2023) - - @pytest.mark.asyncio - async def test_get_metadata_storage_function(self): - storage = await self.substrate.get_metadata_storage_function("System", "Account") - self.assertEqual(storage['module_name'], "System") - self.assertEqual(storage['storage_name'], "Account") - self.assertEqual(storage['spec_version'], 2023) - - @pytest.mark.asyncio - async def test_get_metadata_error(self): - error = await self.substrate.get_metadata_error("System", "InvalidSpecName") - self.assertEqual(error['module_name'], "System") - self.assertEqual(error['error_name'], "InvalidSpecName") - self.assertEqual(error['spec_version'], 2023) - - -if __name__ == '__main__': - unittest.main() diff --git a/tests/unit_tests/subtensor/interface/test_keypair.py b/tests/unit_tests/subtensor/interface/test_keypair.py deleted file mode 100644 index 6804173200..0000000000 --- a/tests/unit_tests/subtensor/interface/test_keypair.py +++ /dev/null @@ -1,215 +0,0 @@ -# Python Substrate Interface Library -# -# Copyright 2018-2020 Stichting Polkascan (Polkascan Foundation). -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import unittest - -from scalecodec import ScaleBytes -from bittensor.subtensor.interface import Keypair, KeypairType, extract_derive_path, ConfigurationError, DEV_PHRASE -from bip39 import bip39_validate - - -class KeyPairTestCase(unittest.TestCase): - - def test_generate_mnemonic(self): - mnemonic = Keypair.generate_mnemonic() - self.assertTrue(bip39_validate(mnemonic)) - - def test_invalid_mnemic(self): - mnemonic = "This is an invalid mnemonic" - self.assertFalse(bip39_validate(mnemonic)) - - def test_create_sr25519_keypair(self): - mnemonic = "old leopard transfer rib spatial phone calm indicate online fire caution review" - keypair = Keypair.create_from_mnemonic(mnemonic, address_type=0) - - self.assertEqual(keypair.ss58_address, "16ADqpMa4yzfmWs3nuTSMhfZ2ckeGtvqhPWCNqECEGDcGgU2") - - def test_only_provide_ss58_address(self): - - keypair = Keypair(ss58_address='16ADqpMa4yzfmWs3nuTSMhfZ2ckeGtvqhPWCNqECEGDcGgU2') - self.assertEqual(keypair.public_key, '0xe4359ad3e2716c539a1d663ebd0a51bdc5c98a12e663bb4c4402db47828c9446') - - def test_only_provide_public_key(self): - - keypair = Keypair( - public_key='0xe4359ad3e2716c539a1d663ebd0a51bdc5c98a12e663bb4c4402db47828c9446', - address_type=0 - ) - self.assertEqual(keypair.ss58_address, '16ADqpMa4yzfmWs3nuTSMhfZ2ckeGtvqhPWCNqECEGDcGgU2') - - def test_provide_no_ss58_address_and_public_key(self): - self.assertRaises(ValueError, Keypair) - - def test_incorrect_private_key_length_sr25519(self): - self.assertRaises( - ValueError, Keypair, private_key='0x23', ss58_address='16ADqpMa4yzfmWs3nuTSMhfZ2ckeGtvqhPWCNqECEGDcGgU2' - ) - - def test_incorrect_public_key(self): - self.assertRaises(ValueError, Keypair, public_key='0x23') - - def test_sign_and_verify(self): - mnemonic = Keypair.generate_mnemonic() - keypair = Keypair.create_from_mnemonic(mnemonic) - signature = keypair.sign("Test123") - self.assertTrue(keypair.verify("Test123", signature)) - - def test_sign_and_verify_hex_data(self): - mnemonic = Keypair.generate_mnemonic() - keypair = Keypair.create_from_mnemonic(mnemonic) - signature = keypair.sign("0x1234") - self.assertTrue(keypair.verify("0x1234", signature)) - - def test_sign_and_verify_scale_bytes(self): - mnemonic = Keypair.generate_mnemonic() - keypair = Keypair.create_from_mnemonic(mnemonic) - - data = ScaleBytes('0x1234') - - signature = keypair.sign(data) - self.assertTrue(keypair.verify(data, signature)) - - def test_sign_missing_private_key(self): - keypair = Keypair(ss58_address="5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY") - self.assertRaises(ConfigurationError, keypair.sign, "0x1234") - - def test_sign_unsupported_crypto_type(self): - keypair = Keypair.create_from_private_key( - ss58_address='16ADqpMa4yzfmWs3nuTSMhfZ2ckeGtvqhPWCNqECEGDcGgU2', - private_key='0x1f1995bdf3a17b60626a26cfe6f564b337d46056b7a1281b64c649d592ccda0a9cffd34d9fb01cae1fba61aeed184c817442a2186d5172416729a4b54dd4b84e', - crypto_type=3 - ) - self.assertRaises(ConfigurationError, keypair.sign, "0x1234") - - def test_verify_unsupported_crypto_type(self): - keypair = Keypair.create_from_private_key( - ss58_address='16ADqpMa4yzfmWs3nuTSMhfZ2ckeGtvqhPWCNqECEGDcGgU2', - private_key='0x1f1995bdf3a17b60626a26cfe6f564b337d46056b7a1281b64c649d592ccda0a9cffd34d9fb01cae1fba61aeed184c817442a2186d5172416729a4b54dd4b84e', - crypto_type=3 - ) - self.assertRaises(ConfigurationError, keypair.verify, "0x1234", '0x1234') - - def test_sign_and_verify_incorrect_signature(self): - mnemonic = Keypair.generate_mnemonic() - keypair = Keypair.create_from_mnemonic(mnemonic) - signature = "0x4c291bfb0bb9c1274e86d4b666d13b2ac99a0bacc04a4846fb8ea50bda114677f83c1f164af58fc184451e5140cc8160c4de626163b11451d3bbb208a1889f8a" - self.assertFalse(keypair.verify("Test123", signature)) - - def test_sign_and_verify_invalid_signature(self): - mnemonic = Keypair.generate_mnemonic() - keypair = Keypair.create_from_mnemonic(mnemonic) - signature = "Test" - self.assertRaises(TypeError, keypair.verify, "Test123", signature) - - def test_sign_and_verify_invalid_message(self): - mnemonic = Keypair.generate_mnemonic() - keypair = Keypair.create_from_mnemonic(mnemonic) - signature = keypair.sign("Test123") - self.assertFalse(keypair.verify("OtherMessage", signature)) - - def test_create_ed25519_keypair(self): - mnemonic = "old leopard transfer rib spatial phone calm indicate online fire caution review" - keypair = Keypair.create_from_mnemonic(mnemonic, address_type=0, crypto_type=KeypairType.ED25519) - - self.assertEqual(keypair.ss58_address, "16dYRUXznyhvWHS1ktUENGfNAEjCawyDzHRtN9AdFnJRc38h") - - def test_sign_and_verify_ed25519(self): - mnemonic = Keypair.generate_mnemonic() - keypair = Keypair.create_from_mnemonic(mnemonic, crypto_type=KeypairType.ED25519) - signature = keypair.sign("Test123") - - self.assertTrue(keypair.verify("Test123", signature)) - - def test_sign_and_verify_invalid_signature_ed25519(self): - mnemonic = Keypair.generate_mnemonic() - keypair = Keypair.create_from_mnemonic(mnemonic, crypto_type=KeypairType.ED25519) - signature = "0x4c291bfb0bb9c1274e86d4b666d13b2ac99a0bacc04a4846fb8ea50bda114677f83c1f164af58fc184451e5140cc8160c4de626163b11451d3bbb208a1889f8a" - self.assertFalse(keypair.verify("Test123", signature)) - - def test_unsupport_crypto_type(self): - self.assertRaises( - ValueError, Keypair.create_from_seed, - seed_hex='0xda3cf5b1e9144931?a0f0db65664aab662673b099415a7f8121b7245fb0be4143', - crypto_type=2 - ) - - def test_create_keypair_from_private_key(self): - keypair = Keypair.create_from_private_key( - ss58_address='16ADqpMa4yzfmWs3nuTSMhfZ2ckeGtvqhPWCNqECEGDcGgU2', - private_key='0x1f1995bdf3a17b60626a26cfe6f564b337d46056b7a1281b64c649d592ccda0a9cffd34d9fb01cae1fba61aeed184c817442a2186d5172416729a4b54dd4b84e' - ) - self.assertEqual(keypair.public_key, '0xe4359ad3e2716c539a1d663ebd0a51bdc5c98a12e663bb4c4402db47828c9446') - - def test_hdkd_hard_path(self): - mnemonic = 'old leopard transfer rib spatial phone calm indicate online fire caution review' - derivation_address = '5FEiH8iuDUw271xbqWTWuB6WrDjv5dnCeDX1CyHubAniXDNN' - derivation_path = '//Alice' - - derived_keypair = Keypair.create_from_uri(mnemonic + derivation_path) - - self.assertEqual(derivation_address, derived_keypair.ss58_address) - - def test_hdkd_soft_path(self): - mnemonic = 'old leopard transfer rib spatial phone calm indicate online fire caution review' - derivation_address = '5GNXbA46ma5dg19GXdiKi5JH3mnkZ8Yea3bBtZAvj7t99P9i' - derivation_path = '/Alice' - - derived_keypair = Keypair.create_from_uri(mnemonic + derivation_path) - - self.assertEqual(derivation_address, derived_keypair.ss58_address) - - def test_hdkd_default_to_dev_mnemonic(self): - derivation_address = '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY' - derivation_path = '//Alice' - - derived_keypair = Keypair.create_from_uri(derivation_path) - - self.assertEqual(derivation_address, derived_keypair.ss58_address) - - def test_hdkd_nested_hard_soft_path(self): - derivation_address = '5CJGwWiKXSE16WJaxBdPZhWqUYkotgenLUALv7ZvqQ4TXeqf' - derivation_path = '//Bob/test' - - derived_keypair = Keypair.create_from_uri(derivation_path) - - self.assertEqual(derivation_address, derived_keypair.ss58_address) - - def test_hdkd_nested_soft_hard_path(self): - derivation_address = '5Cwc8tShrshDJUp1P1M21dKUTcYQpV9GcfSa4hUBNmMdV3Cx' - derivation_path = '/Bob//test' - - derived_keypair = Keypair.create_from_uri(derivation_path) - - self.assertEqual(derivation_address, derived_keypair.ss58_address) - - def test_hdkd_path_gt_32_bytes(self): - derivation_address = '5GR5pfZeNs1uQiSWVxZaQiZou3wdZiX894eqgvfNfHbEh7W2' - derivation_path = '//PathNameLongerThan32BytesWhichShouldBeHashed' - - derived_keypair = Keypair.create_from_uri(derivation_path) - - self.assertEqual(derivation_address, derived_keypair.ss58_address) - - def test_hdkd_unsupported_password(self): - self.assertRaises(NotImplementedError, Keypair.create_from_uri, DEV_PHRASE + '///test') - - def test_reconstruct_path_fail(self): - self.assertRaises(ValueError, extract_derive_path, 'no_slashes') - self.assertRaises(ValueError, extract_derive_path, '//') - - -if __name__ == '__main__': - unittest.main() diff --git a/tests/unit_tests/subtensor/interface/test_runtime_state.py b/tests/unit_tests/subtensor/interface/test_runtime_state.py deleted file mode 100644 index 251c46a7f2..0000000000 --- a/tests/unit_tests/subtensor/interface/test_runtime_state.py +++ /dev/null @@ -1,147 +0,0 @@ -# Python Substrate Interface Library -# -# Copyright 2018-2020 Stichting Polkascan (Polkascan Foundation). -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import unittest -import pytest -from unittest.mock import MagicMock - -from scalecodec import ScaleBytes -from scalecodec.metadata import MetadataDecoder - -from bittensor.subtensor.interface import SubstrateWSInterface -from .fixtures import metadata_v12_hex - - -class TestRuntimeState(unittest.TestCase): - - @classmethod - def setUpClass(cls): - - cls.substrate = SubstrateWSInterface(host='dummy', port=666, address_type=42, type_registry_preset='kusama') - - @pytest.mark.asyncio - async def test_plaintype_call(self): - - def mocked_request(method, params): - if method == 'chain_getRuntimeVersion': - return { - "jsonrpc": "2.0", - "result": {"specVersion": 2023}, - "id": 1 - } - if method == 'state_getStorageAt': - return { - "jsonrpc": "2.0", - "result": '0x0800000000000000482d7c0900000000020000000100000000000000000000000000020000', - "id": 1 - } - - metadata_decoder = MetadataDecoder(ScaleBytes(metadata_v12_hex)) - metadata_decoder.decode() - self.substrate.get_block_metadata = MagicMock(return_value=metadata_decoder) - self.substrate.rpc_request = MagicMock(side_effect=mocked_request) - - response = await self.substrate.get_runtime_state( - module='System', - storage_function='Events' - ) - - self.assertEqual(len(response['result']), 2) - - self.assertEqual(response['result'][0]['module_id'], 'System') - self.assertEqual(response['result'][0]['event_id'], 'ExtrinsicSuccess') - self.assertEqual(response['result'][1]['module_id'], 'System') - self.assertEqual(response['result'][1]['event_id'], 'ExtrinsicSuccess') - - @pytest.mark.asyncio - async def test_maptype_call(self): - - def mocked_request(method, params): - if method == 'chain_getRuntimeVersion': - return { - "jsonrpc": "2.0", - "result": {"specVersion": 2023}, - "id": 1 - } - elif method == 'state_getStorageAt': - return { - 'jsonrpc': '2.0', - 'result': '0x00000000030000c16ff28623000000000000000000000000000000000000000000000000000000c16ff286230000000000000000000000c16ff28623000000000000000000', - 'id': 1 - } - - self.substrate.rpc_request = MagicMock(side_effect=mocked_request) - metadata_decoder = MetadataDecoder(ScaleBytes(metadata_v12_hex)) - metadata_decoder.decode() - self.substrate.get_block_metadata = MagicMock(return_value=metadata_decoder) - - response = await self.substrate.get_runtime_state( - module='System', - storage_function='Account', - params=['5GNJqTPyNqANBkUVMN1LPPrxXnFouWXoe2wNSmmEoLctxiZY'] - ) - - self.assertEqual(response['result'], { - 'data': - { - 'feeFrozen': 10000000000000000, - 'free': 10000000000000000, - 'miscFrozen': 10000000000000000, - 'reserved': 0 - }, - 'nonce': 0, - 'refcount': 3 - }) - - @pytest.mark.asyncio - async def test_iterate_map(self): - - def mocked_request(method, params): - if method == 'chain_getRuntimeVersion': - return { - "jsonrpc": "2.0", - "result": {"specVersion": 2023}, - "id": 1 - } - elif method == 'state_getPairs': - return { - "jsonrpc": "2.0", - "result": [ - ['0x5f3e4907f716ac89b6347d15ececedca3ed14b45ed20d054f05e37e2542cfe70e535263148daaf49be5ddb1579b72e84524fc29e78609e3caf42e85aa118ebfe0b0ad404b5bdd25f', - '0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d'] - ], - "id": 1 - } - - self.substrate.rpc_request = MagicMock(side_effect=mocked_request) - metadata_decoder = MetadataDecoder(ScaleBytes(metadata_v12_hex)) - metadata_decoder.decode() - self.substrate.get_block_metadata = MagicMock(return_value=metadata_decoder) - - all_bonded_stash_ctrls = self.substrate.iterate_map( - module='Staking', - storage_function='Bonded', - block_hash='0x7d56e0ff8d3c57f77ea6a1eeef1cd2c0157a7b24d5a1af0f802ca242617922bf' - ) - - self.assertEqual(all_bonded_stash_ctrls, [[ - '0xbe5ddb1579b72e84524fc29e78609e3caf42e85aa118ebfe0b0ad404b5bdd25f', - '0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d' - ]]) - - -if __name__ == '__main__': - unittest.main() diff --git a/tests/unit_tests/subtensor/interface/test_type_registry.py b/tests/unit_tests/subtensor/interface/test_type_registry.py deleted file mode 100644 index 627f65cb3f..0000000000 --- a/tests/unit_tests/subtensor/interface/test_type_registry.py +++ /dev/null @@ -1,73 +0,0 @@ -# Python Substrate Interface Library -# -# Copyright 2018-2020 Stichting Polkascan (Polkascan Foundation). -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os -import sys - -sys.path.append(os.path.abspath('../../py-scale-codec')) - -import unittest -import pytest - -from scalecodec.base import RuntimeConfiguration, ScaleType - -from bittensor.subtensor.interface import SubstrateWSInterface, Keypair, SubstrateRequestException -from .settings import * - - -class KusamaTypeRegistryTestCase(unittest.TestCase): - - @classmethod - def setUpClass(cls): - cls.substrate = SubstrateWSInterface( - host="dummy", - port=666, - address_type=2, - type_registry_preset='kusama' - ) - - @pytest.mark.asyncio - async def test_type_registry_compatibility(self): - - for scale_type in await self.substrate.get_type_registry(): - obj = RuntimeConfiguration().get_decoder_class(scale_type) - - self.assertIsNotNone(obj, '{} not supported'.format(scale_type)) - - -class PolkadotTypeRegistryTestCase(unittest.TestCase): - - @classmethod - def setUpClass(cls): - cls.substrate = SubstrateWSInterface( - host='dummy', - port=666, - address_type=0, - type_registry_preset='polkadot' - ) - - @pytest.mark.asyncio - async def test_type_registry_compatibility(self): - - for scale_type in await self.substrate.get_type_registry(): - - obj = RuntimeConfiguration().get_decoder_class(scale_type) - - self.assertIsNotNone(obj, '{} not supported'.format(scale_type)) - - -if __name__ == '__main__': - unittest.main() diff --git a/tests/unit_tests/test_async_subtensor.py b/tests/unit_tests/test_async_subtensor.py new file mode 100644 index 0000000000..28decac35a --- /dev/null +++ b/tests/unit_tests/test_async_subtensor.py @@ -0,0 +1,4122 @@ +import datetime +import unittest.mock as mock + +import pytest +from async_substrate_interface.types import ScaleObj +from bittensor_wallet import Wallet + +from bittensor import u64_normalized_float +from bittensor.core import async_subtensor +from bittensor.core.async_subtensor import AsyncSubtensor +from bittensor.core.chain_data import ( + proposal_vote_data, + ChainIdentity, + NeuronInfo, + StakeInfo, + SelectiveMetagraphIndex, +) +from bittensor.utils import U64_MAX +from bittensor.utils.balance import Balance +from tests.helpers.helpers import assert_submit_signed_extrinsic + + +@pytest.fixture +def mock_substrate(mocker): + mocked = mocker.patch( + "bittensor.core.async_subtensor.AsyncSubstrateInterface", + autospec=True, + ) + mocked.return_value.get_block_hash = mocker.AsyncMock() + + return mocked.return_value + + +@pytest.fixture +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"} + result = async_subtensor.decode_hex_identity_dict(info_dict) + assert result["info"] == "regular_string" + + +@pytest.mark.asyncio +async def test_init_if_unknown_network_is_valid(mock_substrate): + """Tests __init__ if passed network unknown and is valid.""" + # Preps + fake_valid_endpoint = "wss://blabla.net" + + # Call + subtensor = AsyncSubtensor(fake_valid_endpoint) + + # Asserts + assert subtensor.chain_endpoint == fake_valid_endpoint + assert subtensor.network == "unknown" + + +@pytest.mark.asyncio +async def test_init_if_unknown_network_is_known_endpoint(mock_substrate): + """Tests __init__ if passed network unknown and is valid.""" + # Preps + fake_valid_endpoint = "ws://127.0.0.1:9944" + + # Call + subtensor = AsyncSubtensor(fake_valid_endpoint) + + # Asserts + assert subtensor.chain_endpoint == fake_valid_endpoint + assert subtensor.network == "local" + + +@pytest.mark.asyncio +async def test_init_if_unknown_network_is_not_valid(mock_substrate): + """Tests __init__ if passed network unknown and isn't valid.""" + + # Call + subtensor = AsyncSubtensor("blabla-net") + + # Asserts + assert subtensor.chain_endpoint == "ws://blabla-net" + assert subtensor.network == "unknown" + + +def test__str__return(subtensor): + """Simply tests the result if printing subtensor instance.""" + # Asserts + assert ( + str(subtensor) + == "Network: finney, Chain: wss://entrypoint-finney.opentensor.ai:443" + ) + + +@pytest.mark.asyncio +async def test_async_subtensor_magic_methods(mock_substrate): + """Tests async magic methods of AsyncSubtensor class.""" + + # Call + subtensor = async_subtensor.AsyncSubtensor(network="local") + async with subtensor: + pass + + # Asserts + mock_substrate.initialize.assert_called_once() + mock_substrate.close.assert_called_once() + + +@pytest.mark.parametrize( + "error", + [ConnectionRefusedError, async_subtensor.ssl.SSLError, TimeoutError], +) +@pytest.mark.asyncio +async def test_async_subtensor_aenter_connection_refused_error( + subtensor, mocker, error +): + """Tests __aenter__ method handling all errors.""" + # Preps + fake_async_substrate = mocker.AsyncMock( + autospec=async_subtensor.AsyncSubstrateInterface, + initialize=mocker.AsyncMock(side_effect=error), + ) + mocker.patch.object( + async_subtensor, "AsyncSubstrateInterface", return_value=fake_async_substrate + ) + # Call + subtensor = async_subtensor.AsyncSubtensor(network="local") + + with pytest.raises(ConnectionError): + async with subtensor: + pass + + # Asserts + fake_async_substrate.initialize.assert_awaited_once() + + +@pytest.mark.asyncio +async def test_burned_register(mock_substrate, subtensor, fake_wallet, mocker): + mock_substrate.submit_extrinsic.return_value = mocker.AsyncMock( + is_success=mocker.AsyncMock(return_value=True)(), + ) + mock_substrate.get_payment_info.return_value = {"partial_fee": 10} + mocker.patch.object( + subtensor, + "get_neuron_for_pubkey_and_subnet", + return_value=NeuronInfo.get_null_neuron(), + ) + mocker.patch.object( + subtensor, + "get_balance", + return_value=Balance(1), + ) + + success = await subtensor.burned_register( + fake_wallet, + netuid=1, + ) + + assert success is True + + subtensor.get_neuron_for_pubkey_and_subnet.assert_called_once_with( + fake_wallet.hotkey.ss58_address, + netuid=1, + block_hash=mock_substrate.get_chain_head.return_value, + ) + + assert_submit_signed_extrinsic( + mock_substrate, + fake_wallet.coldkey, + call_module="SubtensorModule", + call_function="burned_register", + call_params={ + "netuid": 1, + "hotkey": fake_wallet.hotkey.ss58_address, + }, + wait_for_finalization=True, + wait_for_inclusion=True, + ) + + +@pytest.mark.asyncio +async def test_burned_register_on_root(mock_substrate, subtensor, fake_wallet, mocker): + mock_substrate.submit_extrinsic.return_value = mocker.AsyncMock( + is_success=mocker.AsyncMock(return_value=True)(), + ) + mocker.patch.object( + subtensor, + "get_balance", + return_value=Balance(1), + ) + mocker.patch.object( + subtensor, + "is_hotkey_registered", + return_value=False, + ) + + success = await subtensor.burned_register( + fake_wallet, + netuid=0, + ) + + assert success is True + + subtensor.is_hotkey_registered.assert_called_once_with( + netuid=0, + hotkey_ss58=fake_wallet.hotkey.ss58_address, + ) + + assert_submit_signed_extrinsic( + mock_substrate, + fake_wallet.coldkey, + call_module="SubtensorModule", + call_function="root_register", + call_params={ + "hotkey": fake_wallet.hotkey.ss58_address, + }, + wait_for_finalization=True, + wait_for_inclusion=True, + ) + + +@pytest.mark.asyncio +async def test_encode_params(subtensor, mocker): + """Tests encode_params happy path.""" + # Preps + subtensor.substrate.create_scale_object = mocker.AsyncMock( + autospec=async_subtensor.AsyncSubstrateInterface.create_scale_object + ) + subtensor.substrate.create_scale_object.return_value.encode = mocker.Mock( + return_value=b"" + ) + + call_definition = { + "params": [ + {"name": "coldkey", "type": "Vec"}, + {"name": "uid", "type": "u16"}, + ] + } + params = ["coldkey", "uid"] + + # Call + decoded_params = await subtensor.encode_params( + call_definition=call_definition, params=params + ) + + # Asserts + subtensor.substrate.create_scale_object.call_args( + mocker.call("coldkey"), + mocker.call("Vec"), + mocker.call("uid"), + mocker.call("u16"), + ) + assert decoded_params == "0x" + + +@pytest.mark.asyncio +async def test_encode_params_raises_error(subtensor, mocker): + """Tests encode_params with raised error.""" + # Preps + subtensor.substrate.create_scale_object = mocker.AsyncMock( + autospec=async_subtensor.AsyncSubstrateInterface.create_scale_object + ) + subtensor.substrate.create_scale_object.return_value.encode = mocker.Mock( + return_value=b"" + ) + + call_definition = { + "params": [ + {"name": "coldkey", "type": "Vec"}, + ] + } + params = {"undefined param": "some value"} + + # Call and assert + with pytest.raises(ValueError): + await subtensor.encode_params(call_definition=call_definition, params=params) + + subtensor.substrate.create_scale_object.return_value.encode.assert_not_called() + + +@pytest.mark.asyncio +async def test_get_current_block(subtensor): + """Tests get_current_block method.""" + # Call + result = await subtensor.get_current_block() + + # Asserts + subtensor.substrate.get_block_number.assert_called_once() + assert result == subtensor.substrate.get_block_number.return_value + + +@pytest.mark.asyncio +async def test_get_block_hash_without_block_id_aka_none(subtensor): + """Tests get_block_hash method without passed block_id.""" + # Call + result = await subtensor.get_block_hash() + + # Asserts + assert result == subtensor.substrate.get_chain_head.return_value + + +@pytest.mark.asyncio +async def test_get_block_hash_with_block_id(subtensor): + """Tests get_block_hash method with passed block_id.""" + # Call + result = await subtensor.get_block_hash(block=1) + + # Asserts + assert result == subtensor.substrate.get_block_hash.return_value + + +@pytest.mark.asyncio +async def test_is_hotkey_registered_any(subtensor, mocker): + """Tests is_hotkey_registered_any method.""" + # Preps + mocked_get_netuids_for_hotkey = mocker.AsyncMock( + return_value=[1, 2], autospec=subtensor.get_netuids_for_hotkey + ) + subtensor.get_netuids_for_hotkey = mocked_get_netuids_for_hotkey + + # Call + result = await subtensor.is_hotkey_registered_any( + hotkey_ss58="hotkey", block_hash="FAKE_HASH" + ) + + # Asserts + assert result is (len(mocked_get_netuids_for_hotkey.return_value) > 0) + + +@pytest.mark.asyncio +async def test_get_subnet_burn_cost(subtensor, mocker): + """Tests get_subnet_burn_cost method.""" + # Preps + mocked_query_runtime_api = mocker.AsyncMock( + autospec=subtensor.query_runtime_api, return_value=1000 + ) + subtensor.query_runtime_api = mocked_query_runtime_api + fake_block_hash = None + + # Call + result = await subtensor.get_subnet_burn_cost(block_hash=fake_block_hash) + + # Assert + assert result == mocked_query_runtime_api.return_value + mocked_query_runtime_api.assert_called_once_with( + runtime_api="SubnetRegistrationRuntimeApi", + method="get_network_registration_cost", + params=[], + block=None, + block_hash=fake_block_hash, + reuse_block=False, + ) + + +@pytest.mark.asyncio +async def test_get_total_subnets(subtensor, mocker): + """Tests get_total_subnets method.""" + # Preps + mocked_substrate_query = mocker.AsyncMock( + autospec=async_subtensor.AsyncSubstrateInterface.query + ) + subtensor.substrate.query = mocked_substrate_query + fake_block_hash = None + + # Call + result = await subtensor.get_total_subnets(block_hash=fake_block_hash) + + # Assert + assert result == mocked_substrate_query.return_value.value + mocked_substrate_query.assert_called_once_with( + module="SubtensorModule", + storage_function="TotalNetworks", + params=[], + block_hash=fake_block_hash, + reuse_block_hash=False, + ) + + +@pytest.mark.parametrize( + "records, response", + [([(0, True), (1, False), (3, False), (3, True)], [0, 3]), ([], [])], + ids=["with records", "empty-records"], +) +@pytest.mark.asyncio +async def test_get_subnets(subtensor, mocker, records, response): + """Tests get_subnets method with any return.""" + # Preps + fake_result = mocker.AsyncMock(autospec=list) + fake_result.records = records + fake_result.__aiter__.return_value = iter(records) + + mocked_substrate_query_map = mocker.AsyncMock( + autospec=async_subtensor.AsyncSubstrateInterface.query_map, + return_value=fake_result, + ) + + subtensor.substrate.query_map = mocked_substrate_query_map + fake_block_hash = None + + # Call + result = await subtensor.get_subnets(block_hash=fake_block_hash) + + # Asserts + mocked_substrate_query_map.assert_called_once_with( + module="SubtensorModule", + storage_function="NetworksAdded", + block_hash=fake_block_hash, + reuse_block_hash=False, + ) + assert result == response + + +@pytest.mark.parametrize( + "hotkey_ss58_in_result", + [True, False], + ids=["hotkey-exists", "hotkey-doesnt-exist"], +) +@pytest.mark.asyncio +async def test_is_hotkey_delegate(subtensor, mocker, hotkey_ss58_in_result): + """Tests is_hotkey_delegate method with any return.""" + # Preps + fake_hotkey_ss58 = "hotkey_58" + mocked_get_delegates = mocker.AsyncMock( + return_value=[ + mocker.Mock(hotkey_ss58=fake_hotkey_ss58 if hotkey_ss58_in_result else "") + ] + ) + subtensor.get_delegates = mocked_get_delegates + + # Call + result = await subtensor.is_hotkey_delegate( + hotkey_ss58=fake_hotkey_ss58, block_hash=None, reuse_block=True + ) + + # Asserts + assert result == hotkey_ss58_in_result + mocked_get_delegates.assert_called_once_with(block_hash=None, reuse_block=True) + + +@pytest.mark.parametrize( + "fake_result, response", [(None, []), ([mock.Mock()], [mock.Mock()])] +) +@pytest.mark.asyncio +async def test_get_delegates(subtensor, mocker, fake_result, response): + """Tests get_delegates method.""" + # Preps + mocked_query_runtime_api = mocker.AsyncMock( + autospec=subtensor.query_runtime_api, return_value=fake_result + ) + subtensor.query_runtime_api = mocked_query_runtime_api + mocked_delegate_info_list_from_dicts = mocker.patch.object( + async_subtensor.DelegateInfo, + "list_from_dicts", + ) + + # Call + result = await subtensor.get_delegates(block_hash=None, reuse_block=False) + + # Asserts + if fake_result: + assert result == mocked_delegate_info_list_from_dicts.return_value + mocked_delegate_info_list_from_dicts.assert_called_once_with(fake_result) + else: + assert result == response + + mocked_query_runtime_api.assert_called_once_with( + runtime_api="DelegateInfoRuntimeApi", + method="get_delegates", + params=[], + block=None, + block_hash=None, + reuse_block=False, + ) + + +@pytest.mark.parametrize( + "fake_result, response", [(None, []), ([mock.Mock()], [mock.Mock()])] +) +@pytest.mark.asyncio +async def test_get_stake_info_for_coldkey(subtensor, mocker, fake_result, response): + """Tests get_stake_info_for_coldkey method.""" + # Preps + fake_coldkey_ss58 = "fake_coldkey_58" + + mocked_query_runtime_api = mocker.AsyncMock( + autospec=subtensor.query_runtime_api, return_value=fake_result + ) + subtensor.query_runtime_api = mocked_query_runtime_api + + mock_stake_info = mocker.Mock( + spec=async_subtensor.StakeInfo, stake=Balance.from_rao(100) + ) + mocked_stake_info_list_from_dicts = mocker.Mock( + return_value=[mock_stake_info] if fake_result else [] + ) + mocker.patch.object( + async_subtensor.StakeInfo, + "list_from_dicts", + mocked_stake_info_list_from_dicts, + ) + + # Call + result = await subtensor.get_stake_info_for_coldkey( + coldkey_ss58=fake_coldkey_ss58, block_hash=None, reuse_block=True + ) + + # Asserts + if fake_result: + mocked_stake_info_list_from_dicts.assert_called_once_with(fake_result) + assert result == mocked_stake_info_list_from_dicts.return_value + else: + assert result == [] + + mocked_query_runtime_api.assert_called_once_with( + runtime_api="StakeInfoRuntimeApi", + method="get_stake_info_for_coldkey", + params=[fake_coldkey_ss58], + block=None, + block_hash=None, + reuse_block=True, + ) + + +@pytest.mark.asyncio +async def test_get_stake_for_coldkey_and_hotkey(subtensor, mocker): + netuids = [1, 2, 3] + block_hash = "valid_block_hash" + stake_info_dict = { + "netuid": 1, + "hotkey": b"\x16:\xech\r\xde,g\x03R1\xb9\x88q\xe79\xb8\x88\x93\xae\xd2)?*\rp\xb2\xe62\xads\x1c", + "coldkey": b"\x16:\xech\r\xde,g\x03R1\xb9\x88q\xe79\xb8\x88\x93\xae\xd2)?*\rp\xb2\xe62\xads\x1c", + "stake": 1, + "locked": False, + "emission": 1, + "drain": 1, + "is_registered": True, + } + query_result = stake_info_dict + expected_result = { + netuid: StakeInfo.from_dict(stake_info_dict) for netuid in netuids + } + + query_fetcher = mocker.AsyncMock(return_value=query_result) + + mocked_query_runtime_api = mocker.patch.object( + subtensor, "query_runtime_api", side_effect=query_fetcher + ) + mocked_determine_block_hash = mocker.patch.object( + subtensor, "determine_block_hash", return_value=block_hash + ) + mocked_get_chain_head = mocker.patch.object( + subtensor.substrate, "get_chain_head", return_value=block_hash + ) + mocked_get_subnets = mocker.patch.object( + subtensor, "get_subnets", return_value=netuids + ) + + result = await subtensor.get_stake_for_coldkey_and_hotkey( + hotkey_ss58="hotkey", coldkey_ss58="coldkey", block_hash=None, netuids=None + ) + + assert result == expected_result + + # validate that mocked functions were called with the right arguments + mocked_query_runtime_api.assert_has_calls( + [ + mock.call( + "StakeInfoRuntimeApi", + "get_stake_info_for_hotkey_coldkey_netuid", + params=["hotkey", "coldkey", netuid], + block_hash=block_hash, + ) + for netuid in netuids + ] + ) + mocked_determine_block_hash.assert_called_once() + mocked_get_chain_head.assert_not_called() + mocked_get_subnets.assert_called_once_with(block_hash=block_hash) + + +@pytest.mark.asyncio +async def test_query_runtime_api(subtensor, mocker): + """Tests query_runtime_api method.""" + # Preps + fake_runtime_api = "DelegateInfoRuntimeApi" + fake_method = "get_delegated" + fake_params = [1, 2, 3] + fake_block_hash = None + reuse_block = False + + mocked_runtime_call = mocker.AsyncMock( + autospec=async_subtensor.AsyncSubstrateInterface.runtime_call + ) + subtensor.substrate.runtime_call = mocked_runtime_call + + mocked_scalecodec = mocker.Mock(autospec=async_subtensor.scalecodec.ScaleBytes) + mocker.patch.object(async_subtensor.scalecodec, "ScaleBytes", mocked_scalecodec) + + # Call + result = await subtensor.query_runtime_api( + runtime_api=fake_runtime_api, + method=fake_method, + params=fake_params, + block_hash=fake_block_hash, + reuse_block=reuse_block, + ) + + # Asserts + mocked_runtime_call.assert_called_once_with( + fake_runtime_api, + fake_method, + fake_params, + fake_block_hash, + ) + + assert result == mocked_runtime_call.return_value.value + + +@pytest.mark.asyncio +async def test_get_balance(subtensor, mocker): + """Tests get_balance method.""" + # Preps + fake_address = "a1" + fake_block = 123 + fake_block_hash = None + reuse_block = True + + mocked_determine_block_hash = mocker.AsyncMock() + mocker.patch.object( + async_subtensor.AsyncSubtensor, + "determine_block_hash", + mocked_determine_block_hash, + ) + + mocked_balance = mocker.patch.object(async_subtensor, "Balance") + + # Call + result = await subtensor.get_balance( + fake_address, fake_block, fake_block_hash, reuse_block + ) + + mocked_determine_block_hash.assert_awaited_once_with( + fake_block, fake_block_hash, reuse_block + ) + subtensor.substrate.query.assert_awaited_once_with( + module="System", + 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 + ) + assert result == mocked_balance.return_value + + +@pytest.mark.parametrize("balance", [100, 100.1]) +@pytest.mark.asyncio +async def test_get_transfer_fee(subtensor, fake_wallet, mocker, balance): + """Tests get_transfer_fee method.""" + # Preps + fake_wallet.coldkeypub = "coldkeypub" + fake_dest = "fake_dest" + fake_value = Balance(balance) + + mocked_compose_call = mocker.AsyncMock() + subtensor.substrate.compose_call = mocked_compose_call + + mocked_get_payment_info = mocker.AsyncMock(return_value={"partial_fee": 100}) + subtensor.substrate.get_payment_info = mocked_get_payment_info + + # Call + result = await subtensor.get_transfer_fee( + wallet=fake_wallet, dest=fake_dest, value=fake_value + ) + + # Assertions + mocked_compose_call.assert_awaited_once() + mocked_compose_call.assert_called_once_with( + call_module="Balances", + call_function="transfer_keep_alive", + call_params={ + "dest": fake_dest, + "value": fake_value.rao, + }, + ) + + assert isinstance(result, async_subtensor.Balance) + mocked_get_payment_info.assert_awaited_once() + mocked_get_payment_info.assert_called_once_with( + call=mocked_compose_call.return_value, keypair="coldkeypub" + ) + + +@pytest.mark.asyncio +async def test_get_transfer_with_exception(subtensor, mocker): + """Tests get_transfer_fee method handle Exception properly.""" + # Preps + fake_value = 123 + + mocked_compose_call = mocker.AsyncMock() + subtensor.substrate.compose_call = mocked_compose_call + subtensor.substrate.get_payment_info.side_effect = Exception + + # Call + result = await subtensor.get_transfer_fee( + wallet=mocker.Mock(), dest=mocker.Mock(), value=fake_value + ) + + # Assertions + assert result == async_subtensor.Balance.from_rao(int(2e7)) + + +@pytest.mark.asyncio +async def test_get_netuids_for_hotkey_with_records(subtensor, mocker): + """Tests get_netuids_for_hotkey method handle records properly.""" + # Preps + records = [] + expected_response = [] + fake_result = mocker.AsyncMock(autospec=list) + fake_result.records = records + fake_result.__aiter__.return_value = iter(records) + + mocked_substrate_query_map = mocker.AsyncMock( + autospec=async_subtensor.AsyncSubstrateInterface.query_map, + return_value=fake_result, + ) + + subtensor.substrate.query_map = mocked_substrate_query_map + fake_hotkey_ss58 = "hotkey_58" + fake_block_hash = None + + # Call + result = await subtensor.get_netuids_for_hotkey( + hotkey_ss58=fake_hotkey_ss58, block_hash=fake_block_hash, reuse_block=True + ) + + # Assertions + mocked_substrate_query_map.assert_called_once_with( + module="SubtensorModule", + storage_function="IsNetworkMember", + params=[fake_hotkey_ss58], + block_hash=fake_block_hash, + reuse_block_hash=True, + ) + assert result == expected_response + + +@pytest.mark.asyncio +async def test_get_netuids_for_hotkey_without_records(subtensor, mocker): + """Tests get_netuids_for_hotkey method handle empty records properly.""" + # Preps + records = [] + expected_response = [] + fake_result = mocker.AsyncMock(autospec=list) + fake_result.records = records + fake_result.__aiter__.return_value = iter(records) + + mocked_substrate_query_map = mocker.AsyncMock( + autospec=async_subtensor.AsyncSubstrateInterface.query_map, + return_value=fake_result, + ) + + subtensor.substrate.query_map = mocked_substrate_query_map + fake_hotkey_ss58 = "hotkey_58" + fake_block_hash = None + + # Call + result = await subtensor.get_netuids_for_hotkey( + hotkey_ss58=fake_hotkey_ss58, block_hash=fake_block_hash, reuse_block=True + ) + + # Assertions + mocked_substrate_query_map.assert_called_once_with( + module="SubtensorModule", + storage_function="IsNetworkMember", + params=[fake_hotkey_ss58], + block_hash=fake_block_hash, + reuse_block_hash=True, + ) + assert result == expected_response + + +@pytest.mark.asyncio +async def test_subnet_exists(subtensor, mocker): + """Tests subnet_exists method .""" + # Preps + fake_netuid = 1 + fake_block_hash = "block_hash" + fake_reuse_block_hash = False + + mocked_substrate_query = mocker.AsyncMock( + autospec=async_subtensor.AsyncSubstrateInterface.query + ) + subtensor.substrate.query = mocked_substrate_query + + # Call + result = await subtensor.subnet_exists( + netuid=fake_netuid, + block_hash=fake_block_hash, + reuse_block=fake_reuse_block_hash, + ) + + # Asserts + mocked_substrate_query.assert_called_once_with( + module="SubtensorModule", + 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 + + +@pytest.mark.asyncio +async def test_get_hyperparameter_happy_path(subtensor, mocker): + """Tests get_hyperparameter method with happy path.""" + # Preps + fake_param_name = "param_name" + fake_netuid = 1 + fake_block_hash = "block_hash" + fake_reuse_block_hash = False + + # kind of fake subnet exists + mocked_subtensor_subnet_exists = mocker.AsyncMock(return_value=True) + subtensor.subnet_exists = mocked_subtensor_subnet_exists + + mocked_substrate_query = mocker.AsyncMock( + autospec=async_subtensor.AsyncSubstrateInterface.query + ) + subtensor.substrate.query = mocked_substrate_query + + # Call + result = await subtensor.get_hyperparameter( + param_name=fake_param_name, + netuid=fake_netuid, + block_hash=fake_block_hash, + reuse_block=fake_reuse_block_hash, + ) + + # Assertions + mocked_subtensor_subnet_exists.assert_called_once() + mocked_substrate_query.assert_called_once_with( + module="SubtensorModule", + 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 + + +@pytest.mark.asyncio +async def test_get_hyperparameter_if_subnet_does_not_exist(subtensor, mocker): + """Tests get_hyperparameter method if subnet does not exist.""" + # Preps + # kind of fake subnet doesn't exist + mocked_subtensor_subnet_exists = mocker.AsyncMock(return_value=False) + subtensor.subnet_exists = mocked_subtensor_subnet_exists + + mocked_substrate_query = mocker.AsyncMock( + autospec=async_subtensor.AsyncSubstrateInterface.query + ) + subtensor.substrate.query = mocked_substrate_query + + # Call + result = await subtensor.get_hyperparameter(mocker.Mock(), mocker.Mock()) + + # Assertions + mocked_subtensor_subnet_exists.assert_called_once() + mocked_substrate_query.assert_not_called() + assert result is None + + +@pytest.mark.parametrize( + "all_netuids, filter_for_netuids, response", + [([1, 2], [3, 4], []), ([1, 2], [1, 3], [1]), ([1, 2], None, [1, 2])], + ids=[ + "all arguments -> no comparison", + "all arguments -> is comparison", + "not filter_for_netuids", + ], +) +@pytest.mark.asyncio +async def test_filter_netuids_by_registered_hotkeys( + subtensor, mocker, all_netuids, filter_for_netuids, response +): + """Tests filter_netuids_by_registered_hotkeys method.""" + # Preps + fake_wallet_1 = mocker.Mock(spec_set=Wallet) + fake_wallet_1.hotkey.ss58_address = "ss58_address_1" + fake_wallet_2 = mocker.Mock(spec_set=Wallet) + fake_wallet_2.hotkey.ss58_address = "ss58_address_2" + + fake_all_netuids = all_netuids + fake_filter_for_netuids = filter_for_netuids + fake_all_hotkeys = [fake_wallet_1, fake_wallet_2] + fake_block_hash = "fake_block_hash" + fake_reuse_block = False + + mocked_get_netuids_for_hotkey = mocker.AsyncMock( + # returned subnets list + return_value=[1, 2] + ) + subtensor.get_netuids_for_hotkey = mocked_get_netuids_for_hotkey + + # Call + + result = await subtensor.filter_netuids_by_registered_hotkeys( + all_netuids=fake_all_netuids, + filter_for_netuids=fake_filter_for_netuids, + all_hotkeys=fake_all_hotkeys, + block_hash=fake_block_hash, + reuse_block=fake_reuse_block, + ) + + # Asserts + mocked_get_netuids_for_hotkey.call_count = len(fake_all_netuids) + assert mocked_get_netuids_for_hotkey.mock_calls == [ + mocker.call( + w.hotkey.ss58_address, + block_hash=fake_block_hash, + reuse_block=fake_reuse_block, + ) + for w in fake_all_hotkeys + ] + assert result == response + + +@pytest.mark.asyncio +async def test_get_existential_deposit_happy_path(subtensor, mocker): + """Tests get_existential_deposit method.""" + # Preps + fake_block_hash = "block_hash" + fake_reuse_block_hash = False + + mocked_substrate_get_constant = mocker.AsyncMock(return_value=mocker.Mock(value=1)) + subtensor.substrate.get_constant = mocked_substrate_get_constant + + spy_balance_from_rao = mocker.spy(async_subtensor.Balance, "from_rao") + + # Call + result = await subtensor.get_existential_deposit( + block_hash=fake_block_hash, reuse_block=fake_reuse_block_hash + ) + + # Asserts + mocked_substrate_get_constant.assert_awaited_once() + mocked_substrate_get_constant.assert_called_once_with( + 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 + ) + assert result == async_subtensor.Balance( + mocked_substrate_get_constant.return_value.value + ) + + +@pytest.mark.asyncio +async def test_get_existential_deposit_raise_exception(subtensor, mocker): + """Tests get_existential_deposit method raise Exception.""" + # Preps + fake_block_hash = "block_hash" + fake_reuse_block_hash = False + + mocked_substrate_get_constant = mocker.AsyncMock(return_value=None) + subtensor.substrate.get_constant = mocked_substrate_get_constant + + spy_balance_from_rao = mocker.spy(async_subtensor.Balance, "from_rao") + + # Call + with pytest.raises(Exception): + await subtensor.get_existential_deposit( + block_hash=fake_block_hash, reuse_block=fake_reuse_block_hash + ) + + # Asserts + mocked_substrate_get_constant.assert_awaited_once() + mocked_substrate_get_constant.assert_called_once_with( + 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() + + +@pytest.mark.asyncio +async def test_neurons(subtensor, mocker): + """Tests neurons method.""" + # Preps + fake_netuid = 1 + fake_block_hash = "block_hash" + fake_reuse_block_hash = False + + mocked_query_runtime_api = mocker.patch.object( + subtensor, "query_runtime_api", return_value="NOT NONE" + ) + mocked_neuron_info_list_from_dicts = mocker.patch.object( + async_subtensor.NeuronInfo, "list_from_dicts" + ) + # Call + result = await subtensor.neurons( + netuid=fake_netuid, + block_hash=fake_block_hash, + reuse_block=fake_reuse_block_hash, + ) + + # Asserts + mocked_query_runtime_api.assert_called_once_with( + runtime_api="NeuronInfoRuntimeApi", + method="get_neurons", + params=[fake_netuid], + block=None, + block_hash=fake_block_hash, + reuse_block=fake_reuse_block_hash, + ) + assert result == mocked_neuron_info_list_from_dicts.return_value + + +@pytest.mark.parametrize( + "fake_result, response", + [(None, []), (mock.Mock(), mock.Mock())], + ids=["none", "with data"], +) +@pytest.mark.asyncio +async def test_neurons_lite(subtensor, mocker, fake_result, response): + """Tests neurons_lite method.""" + # Preps + fake_netuid = 1 + fake_block_hash = "block_hash" + fake_reuse_block_hash = False + + mocked_query_runtime_api = mocker.AsyncMock(return_value=fake_result) + subtensor.query_runtime_api = mocked_query_runtime_api + + mocked_neuron_info_lite_list_from_dicts = mocker.patch.object( + async_subtensor.NeuronInfoLite, "list_from_dicts" + ) + + # Call + result = await subtensor.neurons_lite( + netuid=fake_netuid, + block_hash=fake_block_hash, + reuse_block=fake_reuse_block_hash, + ) + + # Assertions + mocked_query_runtime_api.assert_awaited_once() + mocked_query_runtime_api.assert_called_once_with( + runtime_api="NeuronInfoRuntimeApi", + method="get_neurons_lite", + params=[fake_netuid], + block=None, + block_hash=fake_block_hash, + reuse_block=fake_reuse_block_hash, + ) + if fake_result: + mocked_neuron_info_lite_list_from_dicts.assert_called_once_with(fake_result) + assert result == mocked_neuron_info_lite_list_from_dicts.return_value + else: + mocked_neuron_info_lite_list_from_dicts.assert_not_called() + assert result == [] + + +@pytest.mark.asyncio +async def test_get_neuron_for_pubkey_and_subnet_success(subtensor, mocker): + """Tests successful retrieval of neuron information.""" + # Preps + fake_hotkey = "fake_ss58_address" + fake_netuid = 1 + fake_uid = mocker.Mock(value=123) + fake_result = b"fake_neuron_data" + + mocker.patch.object( + subtensor.substrate, + "query", + return_value=fake_uid, + ) + mocker.patch.object( + subtensor.substrate, + "runtime_call", + return_value=mocker.Mock(value=fake_result), + ) + mocked_neuron_info = mocker.patch.object( + async_subtensor.NeuronInfo, "from_dict", return_value="fake_neuron_info" + ) + + # Call + result = await subtensor.get_neuron_for_pubkey_and_subnet( + hotkey_ss58=fake_hotkey, netuid=fake_netuid + ) + + # Asserts + subtensor.substrate.query.assert_awaited_once() + subtensor.substrate.query.assert_called_once_with( + module="SubtensorModule", + 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( + "NeuronInfoRuntimeApi", + "get_neuron", + [fake_netuid, fake_uid.value], + None, + ) + mocked_neuron_info.assert_called_once_with(fake_result) + assert result == "fake_neuron_info" + + +@pytest.mark.asyncio +async def test_get_neuron_for_pubkey_and_subnet_uid_not_found(subtensor, mocker): + """Tests the case where UID is not found.""" + # Preps + fake_hotkey = "fake_ss58_address" + fake_netuid = 1 + + mocker.patch.object( + subtensor.substrate, + "query", + return_value=None, + ) + mocked_get_null_neuron = mocker.patch.object( + async_subtensor.NeuronInfo, "get_null_neuron", return_value="null_neuron" + ) + + # Call + result = await subtensor.get_neuron_for_pubkey_and_subnet( + hotkey_ss58=fake_hotkey, netuid=fake_netuid + ) + + # Asserts + subtensor.substrate.query.assert_called_once_with( + module="SubtensorModule", + 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" + + +@pytest.mark.asyncio +async def test_get_neuron_for_pubkey_and_subnet_rpc_result_empty(subtensor, mocker): + """Tests the case where RPC result is empty.""" + # Preps + fake_hotkey = "fake_ss58_address" + fake_netuid = 1 + fake_uid = 123 + + mocker.patch.object( + subtensor.substrate, + "query", + return_value=mocker.Mock(value=fake_uid), + ) + mocker.patch.object( + subtensor.substrate, + "runtime_call", + return_value=mocker.Mock(value=None), + ) + mocked_get_null_neuron = mocker.patch.object( + async_subtensor.NeuronInfo, "get_null_neuron", return_value="null_neuron" + ) + + # Call + result = await subtensor.get_neuron_for_pubkey_and_subnet( + hotkey_ss58=fake_hotkey, netuid=fake_netuid + ) + + # Asserts + subtensor.substrate.query.assert_called_once_with( + module="SubtensorModule", + storage_function="Uids", + params=[fake_netuid, fake_hotkey], + block_hash=None, + reuse_block_hash=False, + ) + subtensor.substrate.runtime_call.assert_called_once_with( + "NeuronInfoRuntimeApi", + "get_neuron", + [fake_netuid, fake_uid], + None, + ) + mocked_get_null_neuron.assert_called_once() + assert result == "null_neuron" + + +@pytest.mark.asyncio +async def test_neuron_for_uid_happy_path(subtensor, mocker): + """Tests neuron_for_uid method with happy path.""" + # Preps + fake_uid = 1 + fake_netuid = 2 + fake_block_hash = "block_hash" + + mocked_null_neuron = mocker.patch.object( + async_subtensor.NeuronInfo, + "get_null_neuron", + ) + mocked_neuron_info_from_dict = mocker.patch.object( + async_subtensor.NeuronInfo, + "from_dict", + ) + + # Call + result = await subtensor.neuron_for_uid( + uid=fake_uid, netuid=fake_netuid, block_hash=fake_block_hash + ) + + # Asserts + mocked_null_neuron.assert_not_called() + mocked_neuron_info_from_dict.assert_called_once_with( + subtensor.substrate.runtime_call.return_value.value + ) + assert result == mocked_neuron_info_from_dict.return_value + + +@pytest.mark.asyncio +async def test_neuron_for_uid_with_none_uid(subtensor, mocker): + """Tests neuron_for_uid method when uid is None.""" + # Preps + fake_uid = None + fake_netuid = 1 + fake_block_hash = "block_hash" + + mocked_null_neuron = mocker.patch.object( + async_subtensor.NeuronInfo, + "get_null_neuron", + ) + + # Call + result = await subtensor.neuron_for_uid( + uid=fake_uid, netuid=fake_netuid, block_hash=fake_block_hash + ) + + # Asserts + mocked_null_neuron.assert_called_once() + assert result == mocked_null_neuron.return_value + + +@pytest.mark.asyncio +async def test_neuron_for_uid(subtensor, mocker): + """Tests neuron_for_uid method.""" + # Preps + fake_uid = 1 + fake_netuid = 2 + fake_block_hash = "block_hash" + + mocked_null_neuron = mocker.patch.object( + async_subtensor.NeuronInfo, + "get_null_neuron", + ) + + # no result in response + mocked_substrate_runtime_call = mocker.AsyncMock( + return_value=mocker.Mock( + value=None, + ), + ) + subtensor.substrate.runtime_call = mocked_substrate_runtime_call + + mocked_neuron_info_from_dict = mocker.patch.object( + async_subtensor.NeuronInfo, + "from_dict", + ) + + # Call + result = await subtensor.neuron_for_uid( + uid=fake_uid, netuid=fake_netuid, block_hash=fake_block_hash + ) + + # Asserts + mocked_null_neuron.assert_called_once() + mocked_neuron_info_from_dict.assert_not_called() + assert result == mocked_null_neuron.return_value + + +@pytest.mark.asyncio +async def test_get_delegated_no_block_hash_no_reuse(subtensor, mocker): + """Tests get_delegated method with no block_hash and reuse_block=False.""" + # Preps + fake_coldkey_ss58 = "fake_ss58_address" + + mocked_delegated_list_from_dicts = mocker.patch.object( + async_subtensor.DelegatedInfo, + "list_from_dicts", + ) + + # Call + result = await subtensor.get_delegated(coldkey_ss58=fake_coldkey_ss58) + + # Asserts + subtensor.substrate.runtime_call.assert_called_once_with( + "DelegateInfoRuntimeApi", + "get_delegated", + [fake_coldkey_ss58], + None, + ) + mocked_delegated_list_from_dicts.assert_called_once_with( + subtensor.substrate.runtime_call.return_value.value + ) + assert result == mocked_delegated_list_from_dicts.return_value + + +@pytest.mark.asyncio +async def test_get_delegated_with_block_hash(subtensor, mocker): + """Tests get_delegated method with specified block_hash.""" + # Preps + fake_coldkey_ss58 = "fake_ss58_address" + fake_block_hash = "fake_block_hash" + + mocked_delegated_list_from_dicts = mocker.patch.object( + async_subtensor.DelegatedInfo, + "list_from_dicts", + ) + + # Call + result = await subtensor.get_delegated( + coldkey_ss58=fake_coldkey_ss58, block_hash=fake_block_hash + ) + + # Asserts + subtensor.substrate.runtime_call.assert_called_once_with( + "DelegateInfoRuntimeApi", + "get_delegated", + [fake_coldkey_ss58], + fake_block_hash, + ) + mocked_delegated_list_from_dicts.assert_called_once_with( + subtensor.substrate.runtime_call.return_value.value + ) + assert result == mocked_delegated_list_from_dicts.return_value + + +@pytest.mark.asyncio +async def test_get_delegated_with_reuse_block(subtensor, mocker): + """Tests get_delegated method with reuse_block=True.""" + # Preps + fake_coldkey_ss58 = "fake_ss58_address" + reuse_block = True + + mocked_delegated_list_from_dicts = mocker.patch.object( + async_subtensor.DelegatedInfo, + "list_from_dicts", + ) + + # Call + result = await subtensor.get_delegated( + coldkey_ss58=fake_coldkey_ss58, reuse_block=reuse_block + ) + + # Asserts + subtensor.substrate.runtime_call.assert_called_once_with( + "DelegateInfoRuntimeApi", + "get_delegated", + [fake_coldkey_ss58], + subtensor.substrate.last_block_hash, + ) + mocked_delegated_list_from_dicts.assert_called_once_with( + subtensor.substrate.runtime_call.return_value.value + ) + assert result == mocked_delegated_list_from_dicts.return_value + + +@pytest.mark.asyncio +async def test_get_delegated_with_empty_result(subtensor, mocker): + """Tests get_delegated method when RPC request returns an empty result.""" + # Preps + fake_coldkey_ss58 = "fake_ss58_address" + + mocked_runtime_call = mocker.AsyncMock( + return_value=mocker.Mock( + value=None, + ), + ) + subtensor.substrate.runtime_call = mocked_runtime_call + + # Call + result = await subtensor.get_delegated(coldkey_ss58=fake_coldkey_ss58) + + # Asserts + mocked_runtime_call.assert_called_once_with( + "DelegateInfoRuntimeApi", + "get_delegated", + [fake_coldkey_ss58], + None, + ) + assert result == [] + + +@pytest.mark.asyncio +async def test_query_identity_successful(subtensor, mocker): + """Tests query_identity method with successful identity query.""" + # 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", + } + + mocked_query = mocker.AsyncMock(return_value=fake_identity_info) + subtensor.substrate.query = mocked_query + + # Call + result = await subtensor.query_identity( + coldkey_ss58=fake_coldkey_ss58, block_hash=fake_block_hash + ) + + # Asserts + mocked_query.assert_called_once_with( + module="SubtensorModule", + storage_function="IdentitiesV2", + params=[fake_coldkey_ss58], + block_hash=fake_block_hash, + reuse_block_hash=False, + ) + assert result == ChainIdentity( + additional="Additional", + description="Description", + discord="", + github="https://github.com/opentensor/bittensor", + image="", + name="Name", + url="https://www.example.com", + ) + + +@pytest.mark.asyncio +async def test_query_identity_no_info(subtensor, mocker): + """Tests query_identity method when no identity info is returned.""" + # 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, + ) + 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) + + # Asserts + mocked_query.assert_called_once_with( + module="SubtensorModule", + storage_function="IdentitiesV2", + params=[fake_coldkey_ss58], + block_hash=None, + reuse_block_hash=False, + ) + assert result is None + + +@pytest.mark.asyncio +async def test_weights_successful(subtensor, mocker): + """Tests weights method with successful weight distribution retrieval.""" + # Preps + 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)])), + ] + + async def mock_query_map(**_): + for uid, w in fake_weights: + yield uid, w + + mocker.patch.object(subtensor.substrate, "query_map", side_effect=mock_query_map) + + # Call + result = await subtensor.weights(netuid=fake_netuid, block_hash=fake_block_hash) + + # Asserts + subtensor.substrate.query_map.assert_called_once_with( + module="SubtensorModule", + 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)])] + + +@pytest.mark.asyncio +async def test_bonds(subtensor, mocker): + """Tests bonds method with successful bond distribution retrieval.""" + # Preps + 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)])), + ] + + async def mock_query_map(**_): + for uid, b in fake_bonds: + yield uid, b + + mocker.patch.object(subtensor.substrate, "query_map", side_effect=mock_query_map) + + # Call + result = await subtensor.bonds(netuid=fake_netuid, block_hash=fake_block_hash) + + # Asserts + subtensor.substrate.query_map.assert_called_once_with( + module="SubtensorModule", + 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)])] + + +@pytest.mark.asyncio +async def test_does_hotkey_exist_true(subtensor, mocker): + """Tests does_hotkey_exist method when the hotkey exists and is valid.""" + # Preps + fake_hotkey_ss58 = "valid_hotkey" + fake_block_hash = "block_hash" + fake_query_result = ["decoded_account_id"] + + mocked_query = mocker.AsyncMock(value=fake_query_result) + subtensor.substrate.query = mocked_query + + # Call + result = await subtensor.does_hotkey_exist( + hotkey_ss58=fake_hotkey_ss58, block_hash=fake_block_hash + ) + + # Asserts + mocked_query.assert_called_once_with( + module="SubtensorModule", + storage_function="Owner", + params=[fake_hotkey_ss58], + block_hash=fake_block_hash, + reuse_block_hash=False, + ) + assert result is True + + +@pytest.mark.asyncio +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" + + mocked_query = mocker.patch.object( + subtensor.substrate, "query", return_value=fake_query_result + ) + + # Call + result = await subtensor.does_hotkey_exist(hotkey_ss58=fake_hotkey_ss58) + + # Asserts + mocked_query.assert_called_once_with( + module="SubtensorModule", + storage_function="Owner", + params=[fake_hotkey_ss58], + block_hash=None, + reuse_block_hash=False, + ) + assert result is False + + +@pytest.mark.asyncio +async def test_get_hotkey_owner_successful(subtensor, mocker): + """Tests get_hotkey_owner method when the hotkey exists and has an owner.""" + # Preps + fake_hotkey_ss58 = "valid_hotkey" + fake_block_hash = "block_hash" + + mocked_query = mocker.AsyncMock(return_value="decoded_owner_account_id") + subtensor.substrate.query = mocked_query + + mocked_does_hotkey_exist = mocker.AsyncMock(return_value=True) + subtensor.does_hotkey_exist = mocked_does_hotkey_exist + + # Call + result = await subtensor.get_hotkey_owner( + hotkey_ss58=fake_hotkey_ss58, block_hash=fake_block_hash + ) + + # Asserts + mocked_query.assert_called_once_with( + module="SubtensorModule", + 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 + ) + assert result == "decoded_owner_account_id" + + +@pytest.mark.asyncio +async def test_get_hotkey_owner_non_existent_hotkey(subtensor, mocker): + """Tests get_hotkey_owner method when the hotkey does not exist in the query result.""" + # Preps + fake_hotkey_ss58 = "non_existent_hotkey" + fake_block_hash = "block_hash" + + mocked_query = mocker.AsyncMock(return_value=None) + subtensor.substrate.query = mocked_query + + # Call + result = await subtensor.get_hotkey_owner( + hotkey_ss58=fake_hotkey_ss58, block_hash=fake_block_hash + ) + + # Asserts + mocked_query.assert_called_once_with( + module="SubtensorModule", + storage_function="Owner", + params=[fake_hotkey_ss58], + block_hash=fake_block_hash, + reuse_block_hash=False, + ) + assert result is None + + +@pytest.mark.asyncio +async def test_sign_and_send_extrinsic_success_finalization( + subtensor, fake_wallet, mocker +): + """Tests sign_and_send_extrinsic when the extrinsic is successfully finalized.""" + # Preps + fake_call = mocker.Mock() + fake_extrinsic = mocker.Mock() + fake_response = mocker.Mock() + + mocked_create_signed_extrinsic = mocker.AsyncMock(return_value=fake_extrinsic) + subtensor.substrate.create_signed_extrinsic = mocked_create_signed_extrinsic + + mocked_submit_extrinsic = mocker.AsyncMock(return_value=fake_response) + subtensor.substrate.submit_extrinsic = mocked_submit_extrinsic + + fake_response.process_events = mocker.AsyncMock() + + async def fake_is_success(): + return True + + fake_response.is_success = fake_is_success() + + # Call + result = await subtensor.sign_and_send_extrinsic( + call=fake_call, + wallet=fake_wallet, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + # Asserts + mocked_create_signed_extrinsic.assert_called_once_with( + call=fake_call, keypair=fake_wallet.coldkey + ) + mocked_submit_extrinsic.assert_called_once_with( + fake_extrinsic, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert result == (True, "") + + +@pytest.mark.asyncio +async def test_sign_and_send_extrinsic_error_finalization( + subtensor, fake_wallet, mocker +): + """Tests sign_and_send_extrinsic when the extrinsic is error finalized.""" + # Preps + fake_call = mocker.Mock() + fake_extrinsic = mocker.Mock() + fake_response = mocker.Mock() + + mocked_create_signed_extrinsic = mocker.AsyncMock(return_value=fake_extrinsic) + subtensor.substrate.create_signed_extrinsic = mocked_create_signed_extrinsic + + mocked_submit_extrinsic = mocker.AsyncMock(return_value=fake_response) + subtensor.substrate.submit_extrinsic = mocked_submit_extrinsic + + fake_response.process_events = mocker.AsyncMock() + + async def fake_is_success(): + return False + + fake_response.is_success = fake_is_success() + + async def fake_error_message(): + return {"some error": "message"} + + fake_response.error_message = fake_error_message() + + mocked_format_error_message = mocker.Mock() + async_subtensor.format_error_message = mocked_format_error_message + + # Call + result = await subtensor.sign_and_send_extrinsic( + call=fake_call, + wallet=fake_wallet, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + # Asserts + mocked_create_signed_extrinsic.assert_called_once_with( + call=fake_call, keypair=fake_wallet.coldkey + ) + mocked_submit_extrinsic.assert_called_once_with( + fake_extrinsic, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert result == (False, mocked_format_error_message.return_value) + + +@pytest.mark.asyncio +async def test_sign_and_send_extrinsic_success_without_inclusion_finalization( + subtensor, fake_wallet, mocker +): + """Tests sign_and_send_extrinsic when extrinsic is submitted without waiting for inclusion or finalization.""" + # Preps + fake_call = mocker.Mock() + fake_extrinsic = mocker.Mock() + + mocked_create_signed_extrinsic = mocker.AsyncMock(return_value=fake_extrinsic) + subtensor.substrate.create_signed_extrinsic = mocked_create_signed_extrinsic + + mocked_submit_extrinsic = mocker.AsyncMock() + subtensor.substrate.submit_extrinsic = mocked_submit_extrinsic + + # Call + result = await subtensor.sign_and_send_extrinsic( + call=fake_call, + wallet=fake_wallet, + wait_for_inclusion=False, + wait_for_finalization=False, + ) + + # Asserts + mocked_create_signed_extrinsic.assert_awaited_once() + mocked_create_signed_extrinsic.assert_called_once_with( + call=fake_call, keypair=fake_wallet.coldkey + ) + mocked_submit_extrinsic.assert_awaited_once() + mocked_submit_extrinsic.assert_called_once_with( + fake_extrinsic, + wait_for_inclusion=False, + wait_for_finalization=False, + ) + assert result == (True, "Not waiting for finalization or inclusion.") + + +@pytest.mark.asyncio +async def test_sign_and_send_extrinsic_substrate_request_exception( + subtensor, fake_wallet, mocker +): + """Tests sign_and_send_extrinsic when SubstrateRequestException is raised.""" + # Preps + fake_call = mocker.Mock() + fake_extrinsic = mocker.Mock() + fake_exception = async_subtensor.SubstrateRequestException("Test Exception") + + mocked_create_signed_extrinsic = mocker.AsyncMock(return_value=fake_extrinsic) + subtensor.substrate.create_signed_extrinsic = mocked_create_signed_extrinsic + + mocked_submit_extrinsic = mocker.AsyncMock(side_effect=fake_exception) + subtensor.substrate.submit_extrinsic = mocked_submit_extrinsic + + mocker.patch.object( + async_subtensor, + "format_error_message", + return_value=str(fake_exception), + ) + + # Call + result = await subtensor.sign_and_send_extrinsic( + call=fake_call, + wallet=fake_wallet, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + # Asserts + assert result == (False, str(fake_exception)) + + +@pytest.mark.asyncio +async def test_sign_and_send_extrinsic_raises_error( + mock_substrate, subtensor, fake_wallet, mocker +): + mock_substrate.submit_extrinsic.return_value = mocker.AsyncMock( + error_message=mocker.AsyncMock( + return_value={ + "name": "Exception", + }, + )(), + is_success=mocker.AsyncMock(return_value=False)(), + ) + + with pytest.raises( + async_subtensor.SubstrateRequestException, + match="{'name': 'Exception'}", + ): + await subtensor.sign_and_send_extrinsic( + call=mocker.Mock(), + wallet=fake_wallet, + raise_error=True, + ) + + +@pytest.mark.asyncio +async def test_get_children_success(subtensor, mocker): + """Tests get_children when children are successfully retrieved and formatted.""" + # Preps + fake_hotkey = "valid_hotkey" + fake_netuid = 1 + fake_children = mocker.Mock( + value=[ + (1000, ["child_key_1"]), + (2000, ["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"), + ] + + # Call + result = await subtensor.get_children(hotkey=fake_hotkey, netuid=fake_netuid) + + # Asserts + mocked_query.assert_called_once_with( + block_hash=None, + 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, "") + + +@pytest.mark.asyncio +async def test_get_children_no_children(subtensor, mocker): + """Tests get_children when there are no children to retrieve.""" + # Preps + fake_hotkey = "valid_hotkey" + fake_netuid = 1 + fake_children = [] + + mocked_query = mocker.AsyncMock(return_value=fake_children) + subtensor.substrate.query = mocked_query + + # Call + result = await subtensor.get_children(hotkey=fake_hotkey, netuid=fake_netuid) + + # Asserts + mocked_query.assert_called_once_with( + block_hash=None, + module="SubtensorModule", + storage_function="ChildKeys", + params=[fake_hotkey, fake_netuid], + reuse_block_hash=False, + ) + assert result == (True, [], "") + + +@pytest.mark.asyncio +async def test_get_children_substrate_request_exception(subtensor, mocker): + """Tests get_children when SubstrateRequestException is raised.""" + # Preps + fake_hotkey = "valid_hotkey" + fake_netuid = 1 + fake_exception = async_subtensor.SubstrateRequestException("Test Exception") + + mocked_query = mocker.AsyncMock(side_effect=fake_exception) + subtensor.substrate.query = mocked_query + + mocked_format_error_message = mocker.Mock(return_value="Formatted error message") + mocker.patch.object( + async_subtensor, "format_error_message", mocked_format_error_message + ) + + # Call + result = await subtensor.get_children(hotkey=fake_hotkey, netuid=fake_netuid) + + # Asserts + mocked_query.assert_called_once_with( + block_hash=None, + 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") + + +@pytest.mark.asyncio +async def test_get_parents_success(subtensor, mocker): + """Tests get_parents when parents are successfully retrieved and formatted.""" + # Preps + fake_hotkey = "valid_hotkey" + fake_netuid = 1 + fake_parents = mocker.Mock( + value=[ + (1000, ["parent_key_1"]), + (2000, ["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"), + ] + + # Call + result = await subtensor.get_parents(hotkey=fake_hotkey, netuid=fake_netuid) + + # Asserts + mocked_query.assert_called_once_with( + block_hash=None, + 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 + + +@pytest.mark.asyncio +async def test_get_parents_no_parents(subtensor, mocker): + """Tests get_parents when there are no parents to retrieve.""" + # Preps + fake_hotkey = "valid_hotkey" + fake_netuid = 1 + fake_parents = [] + + mocked_query = mocker.AsyncMock(return_value=fake_parents) + subtensor.substrate.query = mocked_query + + # Call + result = await subtensor.get_parents(hotkey=fake_hotkey, netuid=fake_netuid) + + # Asserts + mocked_query.assert_called_once_with( + block_hash=None, + module="SubtensorModule", + storage_function="ParentKeys", + params=[fake_hotkey, fake_netuid], + reuse_block_hash=False, + ) + assert result == [] + + +@pytest.mark.asyncio +async def test_get_parents_substrate_request_exception(subtensor, mocker): + """Tests get_parents when SubstrateRequestException is raised.""" + # Preps + fake_hotkey = "valid_hotkey" + fake_netuid = 1 + fake_exception = async_subtensor.SubstrateRequestException("Test Exception") + + mocked_query = mocker.AsyncMock(side_effect=fake_exception) + subtensor.substrate.query = mocked_query + + # Call + with pytest.raises(async_subtensor.SubstrateRequestException): + await subtensor.get_parents(hotkey=fake_hotkey, netuid=fake_netuid) + + +@pytest.mark.asyncio +async def test_get_children_pending(mock_substrate, subtensor): + mock_substrate.query.return_value.value = [ + [ + ( + U64_MAX, + (tuple(bytearray(32)),), + ), + ], + 123, + ] + + children, cooldown = await subtensor.get_children_pending( + "hotkey_ss58", + netuid=1, + ) + + assert children == [ + ( + 1.0, + "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM", + ), + ] + assert cooldown == 123 + + mock_substrate.query.assert_called_once_with( + module="SubtensorModule", + storage_function="PendingChildKeys", + params=[1, "hotkey_ss58"], + block_hash=None, + reuse_block_hash=False, + ) + + +@pytest.mark.asyncio +async def test_get_subnet_hyperparameters_success(subtensor, mocker): + """Tests get_subnet_hyperparameters with successful hyperparameter retrieval.""" + # Preps + fake_netuid = 1 + fake_block_hash = "block_hash" + fake_result = object() + + mocked_query_runtime_api = mocker.AsyncMock(return_value=fake_result) + subtensor.query_runtime_api = mocked_query_runtime_api + + mocked_from_dict = mocker.Mock() + mocker.patch.object( + async_subtensor.SubnetHyperparameters, "from_dict", mocked_from_dict + ) + + # Call + result = await subtensor.get_subnet_hyperparameters( + netuid=fake_netuid, block_hash=fake_block_hash + ) + + # Asserts + mocked_query_runtime_api.assert_called_once_with( + runtime_api="SubnetInfoRuntimeApi", + method="get_subnet_hyperparams_v2", + params=[fake_netuid], + block=None, + block_hash=fake_block_hash, + reuse_block=False, + ) + assert result == mocked_from_dict.return_value + + +@pytest.mark.asyncio +async def test_get_subnet_hyperparameters_no_data(subtensor, mocker): + """Tests get_subnet_hyperparameters when no hyperparameters data is returned.""" + # Preps + fake_netuid = 1 + + mocked_query_runtime_api = mocker.AsyncMock(return_value=None) + subtensor.query_runtime_api = mocked_query_runtime_api + + # Call + result = await subtensor.get_subnet_hyperparameters(netuid=fake_netuid) + + # Asserts + mocked_query_runtime_api.assert_called_once_with( + runtime_api="SubnetInfoRuntimeApi", + method="get_subnet_hyperparams_v2", + params=[fake_netuid], + block=None, + block_hash=None, + reuse_block=False, + ) + assert result is None + + +@pytest.mark.asyncio +async def test_get_subnet_hyperparameters_without_0x_prefix(subtensor, mocker): + """Tests get_subnet_hyperparameters when hex_bytes_result is without 0x prefix.""" + # Preps + fake_netuid = 1 + fake_result = object() + + mocked_query_runtime_api = mocker.AsyncMock(return_value=fake_result) + subtensor.query_runtime_api = mocked_query_runtime_api + + mocked_from_dict = mocker.Mock() + mocker.patch.object( + async_subtensor.SubnetHyperparameters, "from_dict", mocked_from_dict + ) + + # Call + result = await subtensor.get_subnet_hyperparameters(netuid=fake_netuid) + + # Asserts + mocked_query_runtime_api.assert_called_once_with( + runtime_api="SubnetInfoRuntimeApi", + method="get_subnet_hyperparams_v2", + params=[fake_netuid], + block=None, + block_hash=None, + reuse_block=False, + ) + mocked_from_dict.assert_called_once_with(fake_result) + assert result == mocked_from_dict.return_value + + +@pytest.mark.asyncio +async def test_get_vote_data_success(subtensor, mocker): + """Tests get_vote_data when voting data is successfully retrieved.""" + # Preps + fake_proposal_hash = "valid_proposal_hash" + 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) + subtensor.substrate.query = mocked_query + + mocked_proposal_vote_data = mocker.Mock() + mocker.patch.object( + async_subtensor.ProposalVoteData, + "from_dict", + return_value=mocked_proposal_vote_data, + ) + + # Call + result = await subtensor.get_vote_data( + proposal_hash=fake_proposal_hash, block_hash=fake_block_hash + ) + + # Asserts + mocked_query.assert_called_once_with( + module="Triumvirate", + storage_function="Voting", + params=[fake_proposal_hash], + block_hash=fake_block_hash, + reuse_block_hash=False, + ) + assert result == mocked_proposal_vote_data + + +@pytest.mark.asyncio +async def test_get_vote_data_no_data(subtensor, mocker): + """Tests get_vote_data when no voting data is available.""" + # Preps + fake_proposal_hash = "invalid_proposal_hash" + fake_block_hash = "block_hash" + + mocked_query = mocker.AsyncMock(return_value=None) + subtensor.substrate.query = mocked_query + + # Call + result = await subtensor.get_vote_data( + proposal_hash=fake_proposal_hash, block_hash=fake_block_hash + ) + + # Asserts + mocked_query.assert_called_once_with( + module="Triumvirate", + storage_function="Voting", + params=[fake_proposal_hash], + block_hash=fake_block_hash, + reuse_block_hash=False, + ) + assert result is None + + +@pytest.mark.asyncio +async def test_get_delegate_identities(subtensor, mocker): + """Tests get_delegate_identities with successful data retrieval from both chain and GitHub.""" + # Preps + fake_block_hash = "block_hash" + fake_chain_data = [ + ( + ["delegate1_ss58"], + mocker.Mock( + value={ + "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": "", + }, + ), + ), + ] + + mocked_query_map = mocker.AsyncMock( + **{"return_value.__aiter__.return_value": iter(fake_chain_data)}, + ) + 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 + ) + + # Call + result = await subtensor.get_delegate_identities(block_hash=fake_block_hash) + + # Asserts + mocked_query_map.assert_called_once_with( + module="SubtensorModule", + storage_function="IdentitiesV2", + block_hash=fake_block_hash, + reuse_block_hash=False, + ) + + assert result["delegate1_ss58"].name == "Chain Delegate 1" + assert result["delegate2_ss58"].name == "Chain Delegate 2" + + +@pytest.mark.asyncio +async def test_is_hotkey_registered_true(subtensor, mocker): + """Tests is_hotkey_registered when the hotkey is registered on the netuid.""" + # Preps + fake_netuid = 1 + fake_hotkey_ss58 = "registered_hotkey" + fake_result = "some_value" + mocked_query = mocker.AsyncMock(return_value=fake_result) + subtensor.substrate.query = mocked_query + + # Call + result = await subtensor.is_hotkey_registered( + netuid=fake_netuid, hotkey_ss58=fake_hotkey_ss58 + ) + + # Asserts + mocked_query.assert_called_once_with( + module="SubtensorModule", + storage_function="Uids", + params=[fake_netuid, fake_hotkey_ss58], + block_hash=None, + reuse_block_hash=False, + ) + assert result is True + + +@pytest.mark.asyncio +async def test_is_hotkey_registered_false(subtensor, mocker): + """Tests is_hotkey_registered when the hotkey is not registered on the netuid.""" + # Preps + fake_netuid = 1 + fake_hotkey_ss58 = "unregistered_hotkey" + fake_result = None + + mocked_query = mocker.AsyncMock(return_value=fake_result) + subtensor.substrate.query = mocked_query + + # Call + result = await subtensor.is_hotkey_registered( + netuid=fake_netuid, hotkey_ss58=fake_hotkey_ss58 + ) + + # Asserts + mocked_query.assert_called_once_with( + module="SubtensorModule", + storage_function="Uids", + params=[fake_netuid, fake_hotkey_ss58], + block_hash=None, + reuse_block_hash=False, + ) + assert result is False + + +@pytest.mark.asyncio +async def test_get_uid_for_hotkey_on_subnet_registered(subtensor, mocker): + """Tests get_uid_for_hotkey_on_subnet when the hotkey is registered and has a UID.""" + # Preps + fake_hotkey_ss58 = "registered_hotkey" + fake_netuid = 1 + fake_block_hash = "block_hash" + fake_uid = 123 + + mocked_query = mocker.AsyncMock(return_value=fake_uid) + subtensor.substrate.query = mocked_query + + # Call + result = await subtensor.get_uid_for_hotkey_on_subnet( + hotkey_ss58=fake_hotkey_ss58, netuid=fake_netuid, block_hash=fake_block_hash + ) + + # Asserts + mocked_query.assert_called_once_with( + module="SubtensorModule", + storage_function="Uids", + params=[fake_netuid, fake_hotkey_ss58], + block_hash=fake_block_hash, + reuse_block_hash=False, + ) + assert result == fake_uid + + +@pytest.mark.asyncio +async def test_get_uid_for_hotkey_on_subnet_not_registered(subtensor, mocker): + """Tests get_uid_for_hotkey_on_subnet when the hotkey is not registered on the subnet.""" + # Preps + fake_hotkey_ss58 = "unregistered_hotkey" + fake_netuid = 1 + fake_block_hash = "block_hash" + fake_result = None + + mocked_query = mocker.AsyncMock(return_value=fake_result) + subtensor.substrate.query = mocked_query + + # Call + result = await subtensor.get_uid_for_hotkey_on_subnet( + hotkey_ss58=fake_hotkey_ss58, netuid=fake_netuid, block_hash=fake_block_hash + ) + + # Asserts + mocked_query.assert_called_once_with( + module="SubtensorModule", + storage_function="Uids", + params=[fake_netuid, fake_hotkey_ss58], + block_hash=fake_block_hash, + reuse_block_hash=False, + ) + assert result is None + + +@pytest.mark.asyncio +async def test_weights_rate_limit_success(subtensor, mocker): + """Tests weights_rate_limit when the hyperparameter value is successfully retrieved.""" + # Preps + fake_netuid = 1 + fake_rate_limit = 10 + + mocked_get_hyperparameter = mocker.patch.object( + subtensor, + "get_hyperparameter", + return_value=fake_rate_limit, + ) + + # Call + result = await subtensor.weights_rate_limit(netuid=fake_netuid) + + # Asserts + mocked_get_hyperparameter.assert_called_once_with( + param_name="WeightsSetRateLimit", + netuid=fake_netuid, + block_hash=None, + reuse_block=False, + ) + assert result == fake_rate_limit + + +@pytest.mark.asyncio +async def test_weights_rate_limit_none(subtensor, mocker): + """Tests weights_rate_limit when the hyperparameter value is not found.""" + # Preps + fake_netuid = 1 + fake_result = None + + mocked_get_hyperparameter = mocker.patch.object( + subtensor, + "get_hyperparameter", + return_value=fake_result, + ) + + # Call + result = await subtensor.weights_rate_limit(netuid=fake_netuid) + + # Asserts + mocked_get_hyperparameter.assert_called_once_with( + param_name="WeightsSetRateLimit", + netuid=fake_netuid, + block_hash=None, + reuse_block=False, + ) + assert result is None + + +@pytest.mark.asyncio +async def test_blocks_since_last_update_success(subtensor, mocker): + """Tests blocks_since_last_update when the data is successfully retrieved.""" + # Preps + fake_netuid = 1 + fake_uid = 5 + last_update_block = 50 + current_block = 100 + fake_blocks_since_update = current_block - last_update_block + + mocked_get_hyperparameter = mocker.patch.object( + subtensor, + "get_hyperparameter", + return_value={fake_uid: last_update_block}, + ) + + mocked_get_current_block = mocker.AsyncMock(return_value=current_block) + subtensor.get_current_block = mocked_get_current_block + + # Call + result = await subtensor.blocks_since_last_update(netuid=fake_netuid, uid=fake_uid) + + # Asserts + mocked_get_hyperparameter.assert_called_once_with( + param_name="LastUpdate", netuid=fake_netuid + ) + mocked_get_current_block.assert_called_once() + assert result == fake_blocks_since_update + + +@pytest.mark.asyncio +async def test_blocks_since_last_update_no_last_update(subtensor, mocker): + """Tests blocks_since_last_update when the last update data is not found.""" + # Preps + fake_netuid = 1 + fake_uid = 5 + fake_result = None + + mocked_get_hyperparameter = mocker.patch.object( + subtensor, + "get_hyperparameter", + return_value=fake_result, + ) + + # Call + result = await subtensor.blocks_since_last_update(netuid=fake_netuid, uid=fake_uid) + + # Asserts + mocked_get_hyperparameter.assert_called_once_with( + param_name="LastUpdate", netuid=fake_netuid + ) + assert result is None + + +@pytest.mark.asyncio +async def test_commit_reveal_enabled(subtensor, mocker): + """Test commit_reveal_enabled.""" + # Preps + netuid = 1 + block_hash = "block_hash" + mocked_get_hyperparameter = mocker.patch.object( + subtensor, "get_hyperparameter", return_value=mocker.AsyncMock() + ) + + # Call + result = await subtensor.commit_reveal_enabled(netuid, block_hash=block_hash) + + # Assertions + mocked_get_hyperparameter.assert_awaited_once_with( + param_name="CommitRevealWeightsEnabled", + block_hash=block_hash, + netuid=netuid, + reuse_block=False, + ) + assert result is False + + +@pytest.mark.asyncio +async def test_get_subnet_reveal_period_epochs(subtensor, mocker): + """Test get_subnet_reveal_period_epochs.""" + # Preps + netuid = 1 + block_hash = "block_hash" + mocked_get_hyperparameter = mocker.patch.object( + subtensor, "get_hyperparameter", return_value=mocker.AsyncMock() + ) + + # Call + result = await subtensor.get_subnet_reveal_period_epochs( + netuid, block_hash=block_hash + ) + + # Assertions + mocked_get_hyperparameter.assert_awaited_once_with( + param_name="RevealPeriodEpochs", block_hash=block_hash, netuid=netuid + ) + assert result == mocked_get_hyperparameter.return_value + + +@pytest.mark.asyncio +async def test_transfer_success(subtensor, fake_wallet, mocker): + """Tests transfer when the transfer is successful.""" + # Preps + fake_destination = "destination_address" + fake_amount = Balance.from_tao(100.0) + fake_transfer_all = False + + mocked_transfer_extrinsic = mocker.AsyncMock(return_value=True) + mocker.patch.object( + async_subtensor, "transfer_extrinsic", mocked_transfer_extrinsic + ) + + # Call + result = await subtensor.transfer( + wallet=fake_wallet, + destination=fake_destination, + amount=fake_amount, + transfer_all=fake_transfer_all, + ) + + # Asserts + mocked_transfer_extrinsic.assert_awaited_once_with( + subtensor=subtensor, + wallet=fake_wallet, + destination=fake_destination, + amount=fake_amount, + transfer_all=fake_transfer_all, + wait_for_inclusion=True, + wait_for_finalization=False, + keep_alive=True, + period=None, + raise_error=False, + ) + assert result == mocked_transfer_extrinsic.return_value + + +@pytest.mark.asyncio +async def test_register_success(subtensor, fake_wallet, mocker): + """Tests register when there is enough balance and registration succeeds.""" + # Preps + fake_netuid = 1 + + mocked_register_extrinsic = mocker.AsyncMock() + mocker.patch.object( + async_subtensor, "register_extrinsic", mocked_register_extrinsic + ) + + # Call + result = await subtensor.register(wallet=fake_wallet, netuid=fake_netuid) + + # Asserts + mocked_register_extrinsic.assert_awaited_once_with( + 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, + period=None, + raise_error=False, + wait_for_finalization=True, + wait_for_inclusion=True, + ) + assert result == mocked_register_extrinsic.return_value + + +@pytest.mark.asyncio +async def test_set_children(subtensor, fake_wallet, mocker): + """Tests set_children extrinsic calls properly.""" + # Preps + mocked_set_children_extrinsic = mocker.AsyncMock() + mocker.patch.object( + async_subtensor, "set_children_extrinsic", mocked_set_children_extrinsic + ) + fake_children = [ + ( + 1.0, + "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM", + ), + ] + + # Call + result = await subtensor.set_children( + fake_wallet, + fake_wallet.hotkey.ss58_address, + netuid=1, + children=fake_children, + ) + + # Asserts + mocked_set_children_extrinsic.assert_awaited_once_with( + subtensor=subtensor, + wallet=fake_wallet, + hotkey=fake_wallet.hotkey.ss58_address, + netuid=1, + children=fake_children, + wait_for_finalization=True, + wait_for_inclusion=True, + raise_error=False, + period=None, + ) + assert result == mocked_set_children_extrinsic.return_value + + +@pytest.mark.asyncio +async def test_set_delegate_take_equal(subtensor, fake_wallet, mocker): + mocker.patch.object(subtensor, "get_delegate_take", return_value=0.18) + + await subtensor.set_delegate_take( + fake_wallet, + fake_wallet.hotkey.ss58_address, + 0.18, + ) + + subtensor.substrate.submit_extrinsic.assert_not_called() + + +@pytest.mark.asyncio +async def test_set_delegate_take_increase( + mock_substrate, subtensor, fake_wallet, mocker +): + mock_substrate.submit_extrinsic.return_value = mocker.Mock( + is_success=mocker.AsyncMock(return_value=True)(), + ) + mocker.patch.object(subtensor, "get_delegate_take", return_value=0.18) + + await subtensor.set_delegate_take( + fake_wallet, + fake_wallet.hotkey.ss58_address, + 0.2, + ) + + assert_submit_signed_extrinsic( + mock_substrate, + fake_wallet.coldkey, + call_module="SubtensorModule", + call_function="increase_take", + call_params={ + "hotkey": fake_wallet.hotkey.ss58_address, + "take": 13107, + }, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + +@pytest.mark.asyncio +async def test_set_delegate_take_decrease( + mock_substrate, subtensor, fake_wallet, mocker +): + mock_substrate.submit_extrinsic.return_value = mocker.Mock( + is_success=mocker.AsyncMock(return_value=True)(), + ) + mocker.patch.object(subtensor, "get_delegate_take", return_value=0.18) + + await subtensor.set_delegate_take( + fake_wallet, + fake_wallet.hotkey.ss58_address, + 0.1, + ) + + assert_submit_signed_extrinsic( + mock_substrate, + fake_wallet.coldkey, + call_module="SubtensorModule", + call_function="decrease_take", + call_params={ + "hotkey": fake_wallet.hotkey.ss58_address, + "take": 6553, + }, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + +@pytest.mark.asyncio +async def test_set_weights_success(subtensor, fake_wallet, mocker): + """Tests set_weights with the successful weight setting on the first try.""" + # Preps + fake_netuid = 1 + fake_uids = [1, 2, 3] + fake_weights = [0.3, 0.5, 0.2] + max_retries = 1 + + mocked_get_uid_for_hotkey_on_subnet = mocker.patch.object( + subtensor, "get_uid_for_hotkey_on_subnet" + ) + subtensor.get_uid_for_hotkey_on_subnet = mocked_get_uid_for_hotkey_on_subnet + + mocked_blocks_since_last_update = mocker.AsyncMock(return_value=2) + subtensor.blocks_since_last_update = mocked_blocks_since_last_update + + mocked_weights_rate_limit = mocker.AsyncMock(return_value=1) + subtensor.weights_rate_limit = mocked_weights_rate_limit + + mocked_set_weights_extrinsic = mocker.AsyncMock(return_value=(True, "Success")) + mocker.patch.object( + async_subtensor, "set_weights_extrinsic", mocked_set_weights_extrinsic + ) + + # Call + result, message = await subtensor.set_weights( + wallet=fake_wallet, + netuid=fake_netuid, + uids=fake_uids, + weights=fake_weights, + max_retries=max_retries, + ) + + # Asserts + mocked_get_uid_for_hotkey_on_subnet.assert_called_once_with( + fake_wallet.hotkey.ss58_address, fake_netuid + ) + mocked_blocks_since_last_update.assert_called_once_with( + fake_netuid, mocked_get_uid_for_hotkey_on_subnet.return_value + ) + mocked_weights_rate_limit.assert_called_once_with(fake_netuid) + mocked_set_weights_extrinsic.assert_called_once_with( + subtensor=subtensor, + wallet=fake_wallet, + netuid=fake_netuid, + uids=fake_uids, + version_key=async_subtensor.version_as_int, + wait_for_finalization=True, + wait_for_inclusion=True, + weights=fake_weights, + period=8, + raise_error=True, + ) + mocked_weights_rate_limit.assert_called_once_with(fake_netuid) + assert result is True + assert message == "Success" + + +@pytest.mark.asyncio +async def test_set_weights_with_exception(subtensor, fake_wallet, mocker): + """Tests set_weights when set_weights_extrinsic raises an exception.""" + # Preps + fake_netuid = 1 + fake_uids = [1, 2, 3] + fake_weights = [0.3, 0.5, 0.2] + fake_uid = 10 + max_retries = 1 + + mocked_get_uid_for_hotkey_on_subnet = mocker.AsyncMock(return_value=fake_uid) + subtensor.get_uid_for_hotkey_on_subnet = mocked_get_uid_for_hotkey_on_subnet + + mocked_blocks_since_last_update = mocker.AsyncMock(return_value=10) + subtensor.blocks_since_last_update = mocked_blocks_since_last_update + + mocked_weights_rate_limit = mocker.AsyncMock(return_value=5) + subtensor.weights_rate_limit = mocked_weights_rate_limit + + mocked_set_weights_extrinsic = mocker.AsyncMock( + side_effect=Exception("Test exception") + ) + mocker.patch.object( + async_subtensor, "set_weights_extrinsic", mocked_set_weights_extrinsic + ) + + # Call + result, message = await subtensor.set_weights( + wallet=fake_wallet, + netuid=fake_netuid, + uids=fake_uids, + weights=fake_weights, + max_retries=max_retries, + ) + + # Asserts + assert mocked_get_uid_for_hotkey_on_subnet.call_count == 1 + assert mocked_blocks_since_last_update.call_count == 1 + assert mocked_weights_rate_limit.call_count == 1 + assert mocked_set_weights_extrinsic.call_count == max_retries + assert result is False + assert message == "No attempt made. Perhaps it is too soon to set weights!" + + +@pytest.mark.asyncio +async def test_commit_weights_success(subtensor, fake_wallet, mocker): + """Tests commit_weights when the weights are committed successfully.""" + # Preps + fake_netuid = 1 + fake_salt = [12345, 67890] + fake_uids = [1, 2, 3] + fake_weights = [100, 200, 300] + max_retries = 3 + + mocked_generate_weight_hash = mocker.Mock(return_value="fake_commit_hash") + mocker.patch.object( + async_subtensor, "generate_weight_hash", mocked_generate_weight_hash + ) + + mocked_commit_weights_extrinsic = mocker.AsyncMock(return_value=(True, "Success")) + mocker.patch.object( + async_subtensor, "commit_weights_extrinsic", mocked_commit_weights_extrinsic + ) + + # Call + result, message = await subtensor.commit_weights( + wallet=fake_wallet, + netuid=fake_netuid, + salt=fake_salt, + uids=fake_uids, + weights=fake_weights, + max_retries=max_retries, + ) + + # Asserts + mocked_generate_weight_hash.assert_called_once_with( + address=fake_wallet.hotkey.ss58_address, + netuid=fake_netuid, + uids=fake_uids, + values=fake_weights, + salt=fake_salt, + version_key=async_subtensor.version_as_int, + ) + mocked_commit_weights_extrinsic.assert_called_once_with( + subtensor=subtensor, + wallet=fake_wallet, + netuid=fake_netuid, + commit_hash="fake_commit_hash", + wait_for_inclusion=False, + wait_for_finalization=False, + period=16, + raise_error=True, + ) + assert result is True + assert message == "Success" + + +@pytest.mark.asyncio +async def test_commit_weights_with_exception(subtensor, fake_wallet, mocker): + """Tests commit_weights when an exception is raised during weight commitment.""" + # Preps + fake_netuid = 1 + fake_salt = [12345, 67890] + fake_uids = [1, 2, 3] + fake_weights = [100, 200, 300] + max_retries = 1 + + mocked_generate_weight_hash = mocker.Mock(return_value="fake_commit_hash") + mocker.patch.object( + async_subtensor, "generate_weight_hash", mocked_generate_weight_hash + ) + + mocked_commit_weights_extrinsic = mocker.AsyncMock( + side_effect=Exception("Test exception") + ) + mocker.patch.object( + async_subtensor, "commit_weights_extrinsic", mocked_commit_weights_extrinsic + ) + + # Call + result, message = await subtensor.commit_weights( + wallet=fake_wallet, + netuid=fake_netuid, + salt=fake_salt, + uids=fake_uids, + weights=fake_weights, + max_retries=max_retries, + ) + + # Asserts + assert mocked_commit_weights_extrinsic.call_count == max_retries + assert result is False + assert "No attempt made. Perhaps it is too soon to commit weights!" in message + + +@pytest.mark.asyncio +async def test_get_all_subnets_info_success(mocker, subtensor): + """Test get_all_subnets_info returns correct data when subnet information is found.""" + # Prep + block = 123 + + mocker.patch.object(subtensor, "query_runtime_api") + mocker.patch.object( + async_subtensor.SubnetInfo, + "list_from_dicts", + ) + + # Call + await subtensor.get_all_subnets_info(block) + + # Asserts + subtensor.query_runtime_api.assert_awaited_once_with( + runtime_api="SubnetInfoRuntimeApi", + method="get_subnets_info_v2", + params=[], + block=block, + block_hash=None, + reuse_block=False, + ) + async_subtensor.SubnetInfo.list_from_dicts.assert_called_once_with( + subtensor.query_runtime_api.return_value, + ) + + +@pytest.mark.asyncio +async def test_set_subnet_identity(mocker, subtensor, fake_wallet): + """Verify that subtensor method `set_subnet_identity` calls proper function with proper arguments.""" + # Preps + fake_netuid = 123 + fake_subnet_identity = mocker.MagicMock() + + mocked_extrinsic = mocker.patch.object( + async_subtensor, "set_subnet_identity_extrinsic" + ) + + # Call + result = await subtensor.set_subnet_identity( + wallet=fake_wallet, netuid=fake_netuid, subnet_identity=fake_subnet_identity + ) + + # Asserts + mocked_extrinsic.assert_awaited_once_with( + subtensor=subtensor, + wallet=fake_wallet, + netuid=fake_netuid, + subnet_name=fake_subnet_identity.subnet_name, + github_repo=fake_subnet_identity.github_repo, + subnet_contact=fake_subnet_identity.subnet_contact, + subnet_url=fake_subnet_identity.subnet_url, + logo_url=fake_subnet_identity.logo_url, + discord=fake_subnet_identity.discord, + description=fake_subnet_identity.description, + additional=fake_subnet_identity.additional, + period=None, + raise_error=False, + wait_for_finalization=True, + wait_for_inclusion=True, + ) + assert result == mocked_extrinsic.return_value + + +@pytest.mark.asyncio +async def test_get_all_neuron_certificates(mocker, subtensor): + fake_netuid = 12 + mocked_query_map_subtensor = mocker.AsyncMock() + mocker.patch.object(subtensor.substrate, "query_map", mocked_query_map_subtensor) + await subtensor.get_all_neuron_certificates(fake_netuid) + mocked_query_map_subtensor.assert_awaited_once_with( + module="SubtensorModule", + 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)) + mocker.patch.object(subtensor.substrate, "query", mocked_query) + expected_result = datetime.datetime( + 2025, 2, 26, 16, 6, 58, tzinfo=datetime.timezone.utc + ) + actual_result = await subtensor.get_timestamp(block=fake_block) + assert expected_result == actual_result + + +@pytest.mark.asyncio +async def test_get_owned_hotkeys_happy_path(subtensor, mocker): + """Tests that the output of get_owned_hotkeys.""" + # Prep + fake_coldkey = "fake_hotkey" + fake_hotkey = "fake_hotkey" + fake_hotkeys = [ + [ + fake_hotkey, + ] + ] + mocked_subtensor = mocker.AsyncMock(return_value=fake_hotkeys) + mocker.patch.object(subtensor.substrate, "query", new=mocked_subtensor) + + mocked_decode_account_id = mocker.Mock() + mocker.patch.object( + async_subtensor, "decode_account_id", new=mocked_decode_account_id + ) + + # Call + result = await subtensor.get_owned_hotkeys(fake_coldkey) + + # Asserts + mocked_subtensor.assert_awaited_once_with( + module="SubtensorModule", + storage_function="OwnedHotkeys", + params=[fake_coldkey], + block_hash=None, + reuse_block_hash=False, + ) + assert result == [mocked_decode_account_id.return_value] + mocked_decode_account_id.assert_called_once_with(fake_hotkey) + + +@pytest.mark.asyncio +async def test_get_owned_hotkeys_return_empty(subtensor, mocker): + """Tests that the output of get_owned_hotkeys is empty.""" + # Prep + fake_coldkey = "fake_hotkey" + mocked_subtensor = mocker.AsyncMock(return_value=[]) + mocker.patch.object(subtensor.substrate, "query", new=mocked_subtensor) + + # Call + result = await subtensor.get_owned_hotkeys(fake_coldkey) + + # Asserts + mocked_subtensor.assert_awaited_once_with( + module="SubtensorModule", + storage_function="OwnedHotkeys", + params=[fake_coldkey], + block_hash=None, + reuse_block_hash=False, + ) + assert result == [] + + +@pytest.mark.asyncio +async def test_start_call(subtensor, mocker): + """Test start_call extrinsic calls properly.""" + # preps + wallet_name = mocker.Mock(spec=Wallet) + netuid = 123 + mocked_extrinsic = mocker.patch.object(async_subtensor, "start_call_extrinsic") + + # Call + result = await subtensor.start_call(wallet_name, netuid) + + # Asserts + mocked_extrinsic.assert_awaited_once_with( + subtensor=subtensor, + wallet=wallet_name, + netuid=netuid, + wait_for_inclusion=True, + wait_for_finalization=False, + period=None, + raise_error=False, + ) + assert result == mocked_extrinsic.return_value + + +@pytest.mark.asyncio +async def test_get_metagraph_info_all_fields(subtensor, mocker): + """Test get_metagraph_info with all fields (default behavior).""" + # Preps + netuid = 1 + mock_value = {"mock": "data"} + + mock_runtime_call = mocker.patch.object( + subtensor.substrate, + "runtime_call", + return_value=mocker.AsyncMock(value=mock_value), + ) + mock_from_dict = mocker.patch.object( + async_subtensor.MetagraphInfo, "from_dict", return_value="parsed_metagraph" + ) + + # Call + result = await subtensor.get_metagraph_info( + netuid=netuid, field_indices=[f for f in range(73)] + ) + + # Asserts + assert result == "parsed_metagraph" + mock_runtime_call.assert_awaited_once_with( + "SubnetInfoRuntimeApi", + "get_selective_metagraph", + params=[netuid, SelectiveMetagraphIndex.all_indices()], + block_hash=await subtensor.determine_block_hash(None), + ) + mock_from_dict.assert_called_once_with(mock_value) + + +@pytest.mark.asyncio +async def test_get_metagraph_info_specific_fields(subtensor, mocker): + """Test get_metagraph_info with specific fields.""" + # Preps + netuid = 1 + mock_value = {"mock": "data"} + fields = [SelectiveMetagraphIndex.Name, 5] + + mock_runtime_call = mocker.patch.object( + subtensor.substrate, + "runtime_call", + return_value=mocker.AsyncMock(value=mock_value), + ) + mock_from_dict = mocker.patch.object( + async_subtensor.MetagraphInfo, "from_dict", return_value="parsed_metagraph" + ) + + # Call + result = await subtensor.get_metagraph_info(netuid=netuid, field_indices=fields) + + # Asserts + assert result == "parsed_metagraph" + mock_runtime_call.assert_awaited_once_with( + "SubnetInfoRuntimeApi", + "get_selective_metagraph", + params=[ + netuid, + [0] + + [ + f.value if isinstance(f, SelectiveMetagraphIndex) else f for f in fields + ], + ], + block_hash=await subtensor.determine_block_hash(None), + ) + mock_from_dict.assert_called_once_with(mock_value) + + +@pytest.mark.parametrize( + "wrong_fields", + [ + [ + "invalid", + ], + [SelectiveMetagraphIndex.Active, 1, "f"], + [1, 2, 3, "f"], + ], +) +@pytest.mark.asyncio +async def test_get_metagraph_info_invalid_field_indices(subtensor, wrong_fields): + """Test get_metagraph_info raises ValueError on invalid field_indices.""" + with pytest.raises( + ValueError, + match="`field_indices` must be a list of SelectiveMetagraphIndex enums or ints.", + ): + await subtensor.get_metagraph_info(netuid=1, field_indices=wrong_fields) + + +@pytest.mark.asyncio +async def test_get_metagraph_info_subnet_not_exist(subtensor, mocker): + """Test get_metagraph_info returns None when subnet doesn't exist.""" + netuid = 1 + mocker.patch.object( + subtensor.substrate, + "runtime_call", + return_value=mocker.AsyncMock(value=None), + ) + + mocked_logger = mocker.Mock() + mocker.patch("bittensor.core.subtensor.logging.error", new=mocked_logger) + + result = await subtensor.get_metagraph_info(netuid=netuid) + + assert result is None + mocked_logger.assert_called_once_with(f"Subnet {netuid} does not exist.") + + +@pytest.mark.asyncio +async def test_blocks_since_last_step_with_value(subtensor, mocker): + """Test blocks_since_last_step returns correct value.""" + # preps + netuid = 1 + block = 123 + mocked_query_subtensor = mocker.AsyncMock() + subtensor.query_subtensor = mocked_query_subtensor + + # call + result = await subtensor.blocks_since_last_step(netuid=netuid, block=block) + + # asserts + mocked_query_subtensor.assert_awaited_once_with( + name="BlocksSinceLastStep", + block=block, + block_hash=None, + reuse_block=False, + params=[netuid], + ) + + assert result == mocked_query_subtensor.return_value.value + + +@pytest.mark.asyncio +async def test_blocks_since_last_step_is_none(subtensor, mocker): + """Test blocks_since_last_step returns None correctly.""" + # preps + netuid = 1 + block = 123 + mocked_query_subtensor = mocker.AsyncMock(return_value=None) + subtensor.query_subtensor = mocked_query_subtensor + + # call + result = await subtensor.blocks_since_last_step(netuid=netuid, block=block) + + # asserts + mocked_query_subtensor.assert_awaited_once_with( + name="BlocksSinceLastStep", + block=block, + block_hash=None, + reuse_block=False, + params=[netuid], + ) + + assert result is None + + +@pytest.mark.asyncio +async def test_get_subnet_owner_hotkey_has_return(subtensor, mocker): + """Test get_subnet_owner_hotkey returns correct value.""" + # preps + netuid = 14 + block = 123 + expected_owner_hotkey = "owner_hotkey" + mocked_query_subtensor = mocker.AsyncMock(return_value=expected_owner_hotkey) + subtensor.query_subtensor = mocked_query_subtensor + + # call + result = await subtensor.get_subnet_owner_hotkey(netuid=netuid, block=block) + + # asserts + mocked_query_subtensor.assert_awaited_once_with( + name="SubnetOwnerHotkey", + block=block, + params=[netuid], + ) + + assert result == expected_owner_hotkey + + +@pytest.mark.asyncio +async def test_get_subnet_owner_hotkey_is_none(subtensor, mocker): + """Test get_subnet_owner_hotkey returns None correctly.""" + # preps + netuid = 14 + block = 123 + mocked_query_subtensor = mocker.AsyncMock(return_value=None) + subtensor.query_subtensor = mocked_query_subtensor + + # call + result = await subtensor.get_subnet_owner_hotkey(netuid=netuid, block=block) + + # asserts + mocked_query_subtensor.assert_awaited_once_with( + name="SubnetOwnerHotkey", + block=block, + params=[netuid], + ) + + assert result is None + + +@pytest.mark.asyncio +async def test_get_subnet_validator_permits_has_values(subtensor, mocker): + """Test get_subnet_validator_permits returns correct value.""" + # preps + netuid = 14 + block = 123 + expected_validator_permits = [False, True, False] + mocked_query_subtensor = mocker.AsyncMock(return_value=expected_validator_permits) + subtensor.query_subtensor = mocked_query_subtensor + + # call + result = await subtensor.get_subnet_validator_permits(netuid=netuid, block=block) + + # asserts + mocked_query_subtensor.assert_awaited_once_with( + name="ValidatorPermit", + block=block, + params=[netuid], + ) + + assert result == expected_validator_permits + + +@pytest.mark.asyncio +async def test_get_subnet_validator_permits_is_none(subtensor, mocker): + """Test get_subnet_validator_permits returns correct value.""" + # preps + netuid = 14 + block = 123 + + mocked_query_subtensor = mocker.AsyncMock(return_value=None) + subtensor.query_subtensor = mocked_query_subtensor + + # call + result = await subtensor.get_subnet_validator_permits(netuid=netuid, block=block) + + # asserts + mocked_query_subtensor.assert_awaited_once_with( + name="ValidatorPermit", + block=block, + params=[netuid], + ) + + assert result is None + + +@pytest.mark.asyncio +async def test_get_subnet_info_success(mocker, subtensor): + """Test get_subnet_info returns correct data when subnet information is found.""" + # Prep + netuid = mocker.Mock() + block = mocker.Mock() + + mocker.patch.object(subtensor, "query_runtime_api") + mocker.patch.object( + async_subtensor.SubnetInfo, + "from_dict", + ) + + # Call + result = await subtensor.get_subnet_info(netuid=netuid, block=block) + + # Asserts + subtensor.query_runtime_api.assert_awaited_once_with( + runtime_api="SubnetInfoRuntimeApi", + method="get_subnet_info_v2", + params=[netuid], + block=block, + block_hash=None, + reuse_block=False, + ) + async_subtensor.SubnetInfo.from_dict.assert_called_once_with( + subtensor.query_runtime_api.return_value, + ) + assert result == async_subtensor.SubnetInfo.from_dict.return_value + + +@pytest.mark.asyncio +async def test_get_subnet_info_no_data(mocker, subtensor): + """Test get_subnet_info returns None.""" + # Prep + netuid = mocker.Mock() + block = mocker.Mock() + mocker.patch.object(async_subtensor.SubnetInfo, "from_dict") + mocker.patch.object(subtensor, "query_runtime_api", return_value=None) + + # Call + result = await subtensor.get_subnet_info(netuid=netuid, block=block) + + # Asserts + subtensor.query_runtime_api.assert_awaited_once_with( + runtime_api="SubnetInfoRuntimeApi", + method="get_subnet_info_v2", + params=[netuid], + block=block, + block_hash=None, + reuse_block=False, + ) + async_subtensor.SubnetInfo.from_dict.assert_not_called() + assert result is None + + +@pytest.mark.parametrize( + "call_return, expected", + [[10, 111], [None, None], [0, 121]], +) +@pytest.mark.asyncio +async def test_get_next_epoch_start_block(mocker, subtensor, call_return, expected): + """Check that get_next_epoch_start_block returns the correct value.""" + # Prep + netuid = mocker.Mock() + block = 20 + + fake_block_hash = mocker.Mock() + mocker.patch.object(subtensor, "get_block_hash", return_value=fake_block_hash) + + mocked_blocks_since_last_step = mocker.AsyncMock(return_value=call_return) + subtensor.blocks_since_last_step = mocked_blocks_since_last_step + + mocker.patch.object(subtensor, "tempo", return_value=100) + + # Call + result = await subtensor.get_next_epoch_start_block(netuid=netuid, block=block) + + # Asserts + mocked_blocks_since_last_step.assert_called_once_with( + netuid=netuid, + block=block, + block_hash=fake_block_hash, + reuse_block=False, + ) + subtensor.tempo.assert_awaited_once_with( + netuid=netuid, block=block, block_hash=fake_block_hash, reuse_block=False + ) + assert result == expected + + +@pytest.mark.asyncio +async def test_unstake_all(subtensor, fake_wallet, mocker): + """Verifies unstake_all calls properly.""" + # Preps + fake_unstake_all_extrinsic = mocker.AsyncMock() + mocker.patch.object( + async_subtensor, "unstake_all_extrinsic", fake_unstake_all_extrinsic + ) + # Call + result = await subtensor.unstake_all( + wallet=fake_wallet, + hotkey=fake_wallet.hotkey.ss58_address, + netuid=1, + ) + # Asserts + fake_unstake_all_extrinsic.assert_awaited_once_with( + subtensor=subtensor, + wallet=fake_wallet, + hotkey=fake_wallet.hotkey.ss58_address, + netuid=1, + rate_tolerance=0.005, + wait_for_inclusion=True, + wait_for_finalization=True, + period=None, + raise_error=False, + ) + assert result == fake_unstake_all_extrinsic.return_value + + +@pytest.mark.asyncio +async def test_get_liquidity_list_subnet_does_not_exits(subtensor, mocker): + """Test get_liquidity_list returns None when subnet doesn't exist.""" + # Preps + mocker.patch.object(subtensor, "subnet_exists", return_value=False) + + # Call + result = await subtensor.get_liquidity_list(wallet=mocker.Mock(), netuid=1) + + # Asserts + subtensor.subnet_exists.assert_awaited_once_with(netuid=1) + assert result is None + + +@pytest.mark.asyncio +async def test_get_liquidity_list_subnet_is_not_active(subtensor, mocker): + """Test get_liquidity_list returns None when subnet is not active.""" + # Preps + mocker.patch.object(subtensor, "subnet_exists", return_value=True) + mocker.patch.object(subtensor, "is_subnet_active", return_value=False) + + # Call + result = await subtensor.get_liquidity_list(wallet=mocker.Mock(), netuid=1) + + # Asserts + subtensor.subnet_exists.assert_awaited_once_with(netuid=1) + subtensor.is_subnet_active.assert_awaited_once_with(netuid=1) + assert result is None + + +@pytest.mark.asyncio +async def test_get_liquidity_list_happy_path(subtensor, fake_wallet, mocker): + """Tests `get_liquidity_list` returns the correct value.""" + # Preps + netuid = 2 + + mocker.patch.object(subtensor, "subnet_exists", return_value=True) + mocker.patch.object(subtensor, "is_subnet_active", return_value=True) + mocker.patch.object(subtensor, "determine_block_hash") + + mocker.patch.object( + async_subtensor, "price_to_tick", return_value=Balance.from_tao(1.0, netuid) + ) + mocker.patch.object( + async_subtensor, + "calculate_fees", + return_value=(Balance.from_tao(0.0), Balance.from_tao(0.0, netuid)), + ) + + mocked_substrate_query_multi = mocker.AsyncMock( + side_effect=[ + [ + (None, {"bits": 0}), + (None, {"bits": 0}), + (None, {"bits": 18446744073709551616}), + ], + [ + ( + None, + { + "liquidity_net": 1000000000000, + "liquidity_gross": 1000000000000, + "fees_out_tao": {"bits": 0}, + "fees_out_alpha": {"bits": 0}, + }, + ), + ( + None, + { + "liquidity_net": -1000000000000, + "liquidity_gross": 1000000000000, + "fees_out_tao": {"bits": 0}, + "fees_out_alpha": {"bits": 0}, + }, + ), + ( + None, + { + "liquidity_net": 1000000000000, + "liquidity_gross": 1000000000000, + "fees_out_tao": {"bits": 0}, + "fees_out_alpha": {"bits": 0}, + }, + ), + ( + None, + { + "liquidity_net": -1000000000000, + "liquidity_gross": 1000000000000, + "fees_out_tao": {"bits": 0}, + "fees_out_alpha": {"bits": 0}, + }, + ), + ( + None, + { + "liquidity_net": 1000000000000, + "liquidity_gross": 1000000000000, + "fees_out_tao": {"bits": 0}, + "fees_out_alpha": {"bits": 0}, + }, + ), + ( + None, + { + "liquidity_net": -1000000000000, + "liquidity_gross": 1000000000000, + "fees_out_tao": {"bits": 0}, + "fees_out_alpha": {"bits": 0}, + }, + ), + ], + ] + ) + + mocker.patch.object( + subtensor.substrate, "query_multi", mocked_substrate_query_multi + ) + + 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,), + mocker.Mock( + value={ + "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}, + } + ), + ], + ] + + fake_result = mocker.AsyncMock(autospec=list) + fake_result.__aiter__.return_value = iter(fake_positions) + + mocked_query_map = mocker.AsyncMock(return_value=fake_result) + mocker.patch.object(subtensor, "query_map", new=mocked_query_map) + + # Call + + result = await subtensor.get_liquidity_list(wallet=fake_wallet, netuid=netuid) + + # Asserts + subtensor.determine_block_hash.assert_awaited_once_with( + block=None, block_hash=None, reuse_block=False + ) + assert async_subtensor.price_to_tick.call_count == 1 + assert async_subtensor.calculate_fees.call_count == len(fake_positions) + + mocked_query_map.assert_awaited_once_with( + module="Swap", + name="Positions", + block=None, + params=[netuid, fake_wallet.coldkeypub.ss58_address], + ) + assert len(result) == len(fake_positions) + assert all([isinstance(p, async_subtensor.LiquidityPosition) for p in result]) + + +@pytest.mark.asyncio +async def test_add_liquidity(subtensor, fake_wallet, mocker): + """Test add_liquidity extrinsic calls properly.""" + # preps + netuid = 123 + mocked_extrinsic = mocker.patch.object(async_subtensor, "add_liquidity_extrinsic") + + # Call + result = await subtensor.add_liquidity( + wallet=fake_wallet, + netuid=netuid, + liquidity=Balance.from_tao(150), + price_low=Balance.from_tao(180).rao, + price_high=Balance.from_tao(130).rao, + ) + + # Asserts + mocked_extrinsic.assert_awaited_once_with( + subtensor=subtensor, + wallet=fake_wallet, + netuid=netuid, + liquidity=Balance.from_tao(150), + price_low=Balance.from_tao(180).rao, + price_high=Balance.from_tao(130).rao, + hotkey=None, + wait_for_inclusion=True, + wait_for_finalization=True, + period=None, + raise_error=False, + ) + assert result == mocked_extrinsic.return_value + + +@pytest.mark.asyncio +async def test_modify_liquidity(subtensor, fake_wallet, mocker): + """Test modify_liquidity extrinsic calls properly.""" + # preps + netuid = 123 + mocked_extrinsic = mocker.patch.object( + async_subtensor, "modify_liquidity_extrinsic" + ) + position_id = 2 + + # Call + result = await subtensor.modify_liquidity( + wallet=fake_wallet, + netuid=netuid, + position_id=position_id, + liquidity_delta=Balance.from_tao(150), + ) + + # Asserts + mocked_extrinsic.assert_awaited_once_with( + subtensor=subtensor, + wallet=fake_wallet, + netuid=netuid, + position_id=position_id, + liquidity_delta=Balance.from_tao(150), + hotkey=None, + wait_for_inclusion=True, + wait_for_finalization=True, + period=None, + raise_error=False, + ) + assert result == mocked_extrinsic.return_value + + +@pytest.mark.asyncio +async def test_remove_liquidity(subtensor, fake_wallet, mocker): + """Test remove_liquidity extrinsic calls properly.""" + # preps + netuid = 123 + mocked_extrinsic = mocker.patch.object( + async_subtensor, "remove_liquidity_extrinsic" + ) + position_id = 2 + + # Call + result = await subtensor.remove_liquidity( + wallet=fake_wallet, + netuid=netuid, + position_id=position_id, + ) + + # Asserts + mocked_extrinsic.assert_awaited_once_with( + subtensor=subtensor, + wallet=fake_wallet, + netuid=netuid, + position_id=position_id, + hotkey=None, + wait_for_inclusion=True, + wait_for_finalization=True, + period=None, + raise_error=False, + ) + assert result == mocked_extrinsic.return_value + + +@pytest.mark.asyncio +async def test_toggle_user_liquidity(subtensor, fake_wallet, mocker): + """Test toggle_user_liquidity extrinsic calls properly.""" + # preps + netuid = 123 + mocked_extrinsic = mocker.patch.object( + async_subtensor, "toggle_user_liquidity_extrinsic" + ) + enable = mocker.Mock() + + # Call + result = await subtensor.toggle_user_liquidity( + wallet=fake_wallet, + netuid=netuid, + enable=enable, + ) + + # Asserts + mocked_extrinsic.assert_awaited_once_with( + subtensor=subtensor, + wallet=fake_wallet, + netuid=netuid, + enable=enable, + wait_for_inclusion=True, + wait_for_finalization=True, + period=None, + raise_error=False, + ) + assert result == mocked_extrinsic.return_value + + +@pytest.mark.asyncio +async def test_get_subnet_price(subtensor, mocker): + """Test get_subnet_price returns the correct value.""" + # preps + netuid = 123 + mocked_determine_block_hash = mocker.patch.object(subtensor, "determine_block_hash") + fake_price = {"bits": 3155343338053956962} + expected_price = Balance.from_tao(0.029258617) + mocked_query = mocker.patch.object( + subtensor.substrate, "query", return_value=fake_price + ) + + # Call + result = await subtensor.get_subnet_price( + netuid=netuid, + ) + + # Asserts + mocked_determine_block_hash.assert_awaited_once_with(block=None) + mocked_query.assert_awaited_once_with( + module="Swap", + storage_function="AlphaSqrtPrice", + params=[netuid], + block_hash=mocked_determine_block_hash.return_value, + ) + + assert result == expected_price + + +@pytest.mark.asyncio +async def test_get_subnet_prices(subtensor, mocker): + """Test get_subnet_prices returns the correct value.""" + # preps + mocked_determine_block_hash = mocker.patch.object(subtensor, "determine_block_hash") + + async def fake_current_sqrt_prices(): + yield [0, {"bits": 0}] + yield [1, {"bits": 3155343338053956962}] + + expected_prices = {0: Balance.from_tao(1), 1: Balance.from_tao(0.029258617)} + mocked_query_map = mocker.patch.object( + subtensor.substrate, "query_map", return_value=fake_current_sqrt_prices() + ) + + # Call + result = await subtensor.get_subnet_prices() + + # Asserts + mocked_determine_block_hash.assert_awaited_once_with( + block=None, block_hash=None, reuse_block=False + ) + mocked_query_map.assert_awaited_once_with( + module="Swap", + storage_function="AlphaSqrtPrice", + block_hash=mocked_determine_block_hash.return_value, + page_size=129, # total number of subnets + ) + assert result == expected_prices + + +@pytest.mark.asyncio +async def test_all_subnets(subtensor, mocker): + """Verify that `all_subnets` calls proper methods and returns the correct value.""" + # Preps + mocked_determine_block_hash = mocker.patch.object(subtensor, "determine_block_hash") + mocked_di_list_from_dicts = mocker.patch.object( + async_subtensor.DynamicInfo, "list_from_dicts" + ) + mocked_get_subnet_prices = mocker.patch.object( + subtensor, + "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 + ) + + # Call + result = await subtensor.all_subnets() + + # Asserts + mocked_determine_block_hash.assert_awaited_once_with( + block=None, block_hash=None, reuse_block=False + ) + subtensor.substrate.runtime_call.assert_called_once_with( + api="SubnetInfoRuntimeApi", + method="get_all_dynamic_info", + block_hash=mocked_determine_block_hash.return_value, + ) + mocked_get_subnet_prices.assert_called_once() + mocked_di_list_from_dicts.assert_called_once_with( + [ + {"netuid": 0, "price": Balance.from_tao(1)}, + {"netuid": 1, "price": Balance.from_tao(0.029258617)}, + ] + ) + assert result == mocked_di_list_from_dicts.return_value + + +@pytest.mark.asyncio +async def test_subnet(subtensor, mocker): + """Verify that `subnet` calls proper methods and returns the correct value.""" + # Preps + netuid = 14 + mocked_determine_block_hash = mocker.patch.object(subtensor, "determine_block_hash") + mocked_di_from_dict = mocker.patch.object(async_subtensor.DynamicInfo, "from_dict") + 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) + mocker.patch.object( + subtensor.substrate, "runtime_call", return_value=mocked_runtime_call + ) + + # Call + result = await subtensor.subnet(netuid=netuid) + + # Asserts + mocked_determine_block_hash.assert_awaited_once_with( + block=None, block_hash=None, reuse_block=False + ) + subtensor.substrate.runtime_call.assert_awaited_once_with( + "SubnetInfoRuntimeApi", + "get_dynamic_info", + params=[netuid], + block_hash=mocked_determine_block_hash.return_value, + ) + mocked_get_subnet_price.assert_awaited_once_with( + netuid=netuid, + block=None, + block_hash=mocked_determine_block_hash.return_value, + reuse_block=False, + ) + mocked_di_from_dict.assert_called_once_with( + {"netuid": netuid, "price": Balance.from_tao(100.0)} + ) + assert result == mocked_di_from_dict.return_value + + +@pytest.mark.asyncio +async def test_get_stake_operations_fee(subtensor, mocker): + """Verify that `get_stake_operations_fee` calls proper methods and returns the correct value.""" + # Preps + netuid = 1 + amount = Balance.from_rao(100_000_000_000) # 100 Tao + mocked_determine_block_hash = mocker.patch.object(subtensor, "determine_block_hash") + mocked_query_map = mocker.patch.object( + subtensor.substrate, "query", return_value=mocker.Mock(value=196) + ) + + # Call + result = await subtensor.get_stake_operations_fee(netuid=netuid, amount=amount) + + # Assert + mocked_determine_block_hash.assert_awaited_once_with( + block=None, block_hash=None, reuse_block=False + ) + mocked_query_map.assert_awaited_once_with( + module="Swap", + storage_function="FeeRate", + params=[netuid], + block_hash=mocked_determine_block_hash.return_value, + ) + assert result == Balance.from_rao(299076829).set_unit(netuid) + + +@pytest.mark.asyncio +async def test_get_stake_add_fee(subtensor, mocker): + """Verify that `get_stake_add_fee` calls proper methods and returns the correct value.""" + # Preps + netuid = mocker.Mock() + amount = mocker.Mock() + mocked_get_stake_operations_fee = mocker.patch.object( + subtensor, "get_stake_operations_fee" + ) + + # Call + result = await subtensor.get_stake_add_fee( + amount=amount, + netuid=netuid, + coldkey_ss58=mocker.Mock(), + hotkey_ss58=mocker.Mock(), + ) + + # Asserts + mocked_get_stake_operations_fee.assert_awaited_once_with( + amount=amount, netuid=netuid, block=None + ) + assert result == mocked_get_stake_operations_fee.return_value + + +@pytest.mark.asyncio +async def test_get_unstake_fee(subtensor, mocker): + """Verify that `get_unstake_fee` calls proper methods and returns the correct value.""" + # Preps + netuid = mocker.Mock() + amount = mocker.Mock() + mocked_get_stake_operations_fee = mocker.patch.object( + subtensor, "get_stake_operations_fee" + ) + + # Call + result = await subtensor.get_unstake_fee( + amount=amount, + netuid=netuid, + coldkey_ss58=mocker.Mock(), + hotkey_ss58=mocker.Mock(), + ) + + # Asserts + mocked_get_stake_operations_fee.assert_awaited_once_with( + amount=amount, netuid=netuid, block=None + ) + assert result == mocked_get_stake_operations_fee.return_value + + +@pytest.mark.asyncio +async def test_get_stake_movement_fee(subtensor, mocker): + """Verify that `get_stake_movement_fee` calls proper methods and returns the correct value.""" + # Preps + netuid = mocker.Mock() + amount = mocker.Mock() + mocked_get_stake_operations_fee = mocker.patch.object( + subtensor, "get_stake_operations_fee" + ) + + # Call + result = await subtensor.get_stake_movement_fee( + amount=amount, + origin_netuid=netuid, + origin_hotkey_ss58=mocker.Mock(), + origin_coldkey_ss58=mocker.Mock(), + destination_netuid=mocker.Mock(), + destination_hotkey_ss58=mocker.Mock(), + destination_coldkey_ss58=mocker.Mock(), + ) + + # Asserts + mocked_get_stake_operations_fee.assert_awaited_once_with( + amount=amount, netuid=netuid, block=None + ) + assert result == mocked_get_stake_operations_fee.return_value + + +@pytest.mark.asyncio +async def test_get_stake_weight(subtensor, mocker): + """Verify that `get_stake_weight` method calls proper methods and returns the correct value.""" + # Preps + netuid = mocker.Mock() + fake_weights = [0, 100, 15000] + expected_result = [0.0, 0.0015259021896696422, 0.22888532845044632] + + mock_determine_block_hash = mocker.patch.object( + subtensor, + "determine_block_hash", + ) + mocked_query = mocker.patch.object( + subtensor.substrate, + "query", + return_value=fake_weights, + ) + + # Call + result = await subtensor.get_stake_weight(netuid=netuid) + + # Asserts + mock_determine_block_hash.assert_awaited_once_with( + block=None, + block_hash=None, + reuse_block=False, + ) + mocked_query.assert_awaited_once_with( + module="SubtensorModule", + storage_function="StakeWeight", + params=[netuid], + block_hash=mock_determine_block_hash.return_value, + ) + assert result == expected_result + + +@pytest.mark.asyncio +async def test_get_timelocked_weight_commits(subtensor, mocker): + """Verify that `get_timelocked_weight_commits` method calls proper methods and returns the correct value.""" + # Preps + netuid = mocker.Mock() + + mock_determine_block_hash = mocker.patch.object( + subtensor, + "determine_block_hash", + ) + mocked_query_map = mocker.AsyncMock( + autospec=async_subtensor.AsyncSubstrateInterface.query_map, + ) + subtensor.substrate.query_map = mocked_query_map + + # Call + result = await subtensor.get_timelocked_weight_commits(netuid=netuid) + + # Asserts + mock_determine_block_hash.assert_awaited_once_with( + block=None, block_hash=None, reuse_block=False + ) + mocked_query_map.assert_awaited_once_with( + module="SubtensorModule", + storage_function="TimelockedWeightCommits", + params=[netuid], + block_hash=mock_determine_block_hash.return_value, + ) + assert result == [] diff --git a/tests/unit_tests/test_axon.py b/tests/unit_tests/test_axon.py new file mode 100644 index 0000000000..d356883721 --- /dev/null +++ b/tests/unit_tests/test_axon.py @@ -0,0 +1,828 @@ +import asyncio +import contextlib +import re +import threading +import time +from dataclasses import dataclass +from typing import Any, Optional, Tuple +from unittest import IsolatedAsyncioTestCase +from unittest.mock import AsyncMock, MagicMock, patch + +import aiohttp +import fastapi +import netaddr +import pydantic +import pytest +import uvicorn +from fastapi.testclient import TestClient +from starlette.requests import Request + +from bittensor.core.axon import Axon, AxonMiddleware, FastAPIThreadedServer +from bittensor.core.errors import RunException +from bittensor.core.settings import version_as_int +from bittensor.core.stream import StreamingSynapse +from bittensor.core.synapse import Synapse +from bittensor.core.threadpool import PriorityThreadPoolExecutor +from bittensor.utils.axon_utils import ( + allowed_nonce_window_ns, + calculate_diff_seconds, + ALLOWED_DELTA, + NANOSECONDS_IN_SECOND, +) + + +def test_attach_initial(mock_get_external_ip): + # Create a mock AxonServer instance + server = Axon() + + # Define the Synapse type + class TestSynapse(Synapse): + pass + + # Define the functions with the correct signatures + def forward_fn(synapse: TestSynapse) -> Any: + pass + + def blacklist_fn(synapse: TestSynapse) -> Tuple[bool, str]: + return True, "" + + def priority_fn(synapse: TestSynapse) -> float: + return 1.0 + + def verify_fn(synapse: TestSynapse) -> None: + pass + + # Test attaching with correct signatures + server.attach(forward_fn, blacklist_fn, priority_fn, verify_fn) + + # Define functions with incorrect signatures + def wrong_blacklist_fn(synapse: TestSynapse) -> int: + return 1 + + def wrong_priority_fn(synapse: TestSynapse) -> int: + return 1 + + def wrong_verify_fn(synapse: TestSynapse) -> bool: + return True + + # Test attaching with incorrect signatures + with pytest.raises(AssertionError): + server.attach(forward_fn, wrong_blacklist_fn, priority_fn, verify_fn) + + with pytest.raises(AssertionError): + server.attach(forward_fn, blacklist_fn, wrong_priority_fn, verify_fn) + + with pytest.raises(AssertionError): + server.attach(forward_fn, blacklist_fn, priority_fn, wrong_verify_fn) + + +def test_attach(mock_get_external_ip): + # Create a mock AxonServer instance + server = Axon() + + # Define the Synapse type + class FakeSynapse: + pass + + # Define a class that inherits from Synapse + class InheritedSynapse(Synapse): + pass + + # Define a function with the correct signature + def forward_fn(synapse: InheritedSynapse) -> Any: + pass + + # Test attaching with correct signature and inherited class + server.attach(forward_fn) + + # Define a class that does not inherit from Synapse + class NonInheritedSynapse: + pass + + # Define a function with an argument of a class not inheriting from Synapse + def wrong_forward_fn(synapse: NonInheritedSynapse) -> Any: + pass + + # Test attaching with incorrect class inheritance + with pytest.raises(AssertionError): + server.attach(wrong_forward_fn) + + +def test_log_and_handle_error(): + from bittensor.core.axon import log_and_handle_error + + synapse = SynapseMock() + + synapse = log_and_handle_error(synapse, Exception("Error"), 500, 100) + assert synapse.axon.status_code == 500 + assert re.match(r"Internal Server Error #[\da-f\-]+", synapse.axon.status_message) + assert synapse.axon.process_time is not None + + +def test_create_error_response(): + from bittensor.core.axon import create_error_response + + synapse = SynapseMock() + synapse.axon.status_code = 500 + synapse.axon.status_message = "Error" + + response = create_error_response(synapse) + assert response.status_code == 500 + assert response.body == b'{"message":"Error"}' + + +# Fixtures +@pytest.fixture +def middleware(): + # Mock AxonMiddleware instance with empty axon object + axon = AxonMock() + return AxonMiddleware(None, axon) + + +@pytest.fixture +def mock_request(): + request = AsyncMock(spec=Request) + request.body = AsyncMock(return_value=b'{"field1": "value1", "field2": "value2"}') + request.url.path = "/test_endpoint" + request.headers = {"computed_body_hash": "correct_hash"} + return request + + +@pytest.fixture +def axon_instance(mock_get_external_ip): + axon = Axon() + axon.required_hash_fields = {"test_endpoint": ["field1", "field2"]} + axon.forward_class_types = { + "test_endpoint": MagicMock(return_value=MagicMock(body_hash="correct_hash")) + } + return axon + + +# Mocks +@dataclass +class MockWallet: + hotkey: Any + coldkey: Any = None + coldkeypub: Any = None + + +class MockHotkey: + def __init__(self, ss58_address): + self.ss58_address = ss58_address + + def sign(self, *args, **kwargs): + return f"Signed: {args!r} {kwargs!r}".encode() + + +class MockInfo: + def to_string(self): + return "MockInfoString" + + +class AxonMock: + def __init__(self): + self.status_code = None + self.forward_class_types = {} + self.blacklist_fns = {} + self.priority_fns = {} + self.forward_fns = {} + self.verify_fns = {} + self.thread_pool = PriorityThreadPoolExecutor(max_workers=1) + + +class SynapseMock(Synapse): + pass + + +def verify_fn_pass(synapse): + pass + + +def verify_fn_fail(synapse): + raise Exception("Verification failed") + + +def blacklist_fn_pass(synapse): + return False, "" + + +def blacklist_fn_fail(synapse): + return True, "" + + +def priority_fn_pass(synapse) -> float: + return 0.0 + + +def priority_fn_timeout(synapse) -> float: + return 2.0 + + +@pytest.mark.asyncio +async def test_verify_pass(middleware): + synapse = SynapseMock() + middleware.axon.verify_fns = {"SynapseMock": verify_fn_pass} + await middleware.verify(synapse) + assert synapse.axon.status_code != 401 + + +@pytest.mark.asyncio +async def test_verify_fail(middleware): + synapse = SynapseMock() + middleware.axon.verify_fns = {"SynapseMock": verify_fn_fail} + with pytest.raises(Exception): + await middleware.verify(synapse) + assert synapse.axon.status_code == 401 + + +@pytest.mark.asyncio +async def test_blacklist_pass(middleware): + synapse = SynapseMock() + middleware.axon.blacklist_fns = {"SynapseMock": blacklist_fn_pass} + await middleware.blacklist(synapse) + assert synapse.axon.status_code != 403 + + +@pytest.mark.asyncio +async def test_blacklist_fail(middleware): + synapse = SynapseMock() + middleware.axon.blacklist_fns = {"SynapseMock": blacklist_fn_fail} + with pytest.raises(Exception): + await middleware.blacklist(synapse) + assert synapse.axon.status_code == 403 + + +@pytest.mark.asyncio +async def test_priority_pass(middleware): + synapse = SynapseMock() + middleware.axon.priority_fns = {"SynapseMock": priority_fn_pass} + await middleware.priority(synapse) + assert synapse.axon.status_code != 408 + + +@pytest.mark.parametrize( + "body, expected", + [ + ( + b'{"field1": "value1", "field2": "value2"}', + {"field1": "value1", "field2": "value2"}, + ), + ( + b'{"field1": "different_value", "field2": "another_value"}', + {"field1": "different_value", "field2": "another_value"}, + ), + ], +) +@pytest.mark.asyncio +async def test_verify_body_integrity_happy_path( + mock_request, axon_instance, body, expected +): + # Arrange + mock_request.body.return_value = body + + # Act + result = await axon_instance.verify_body_integrity(mock_request) + + # Assert + assert result == expected, "The parsed body should match the expected dictionary." + + +@pytest.mark.parametrize( + "body, expected_exception_name", + [ + (b"", "JSONDecodeError"), # Empty body + (b"not_json", "JSONDecodeError"), # Non-JSON body + ], + ids=["empty_body", "non_json_body"], +) +@pytest.mark.asyncio +async def test_verify_body_integrity_edge_cases( + mock_request, axon_instance, body, expected_exception_name +): + # Arrange + mock_request.body.return_value = body + + # Act & Assert + with pytest.raises(Exception) as exc_info: + await axon_instance.verify_body_integrity(mock_request) + assert exc_info.typename == expected_exception_name, "Expected specific exception" + + +@pytest.mark.parametrize( + "computed_hash, expected_error", + [ + ("incorrect_hash", ValueError), + ], +) +@pytest.mark.asyncio +async def test_verify_body_integrity_error_cases( + mock_request, axon_instance, computed_hash, expected_error +): + # Arrange + mock_request.headers["computed_body_hash"] = computed_hash + + # Act & Assert + with pytest.raises(expected_error) as exc_info: + await axon_instance.verify_body_integrity(mock_request) + assert "Hash mismatch" in str(exc_info.value), "Expected a hash mismatch error." + + +@pytest.mark.parametrize( + "info_return, expected_output, test_id", + [ + (MockInfo(), "MockInfoString", "happy_path_basic"), + (MockInfo(), "MockInfoString", "edge_case_empty_string"), + ], +) +def test_to_string(info_return, expected_output, test_id, mock_get_external_ip): + # Arrange + axon = Axon() + with patch.object(axon, "info", return_value=info_return): + # Act + output = axon.to_string() + + # Assert + assert output == expected_output, f"Test ID: {test_id}" + + +@pytest.mark.parametrize( + "ip, port, expected_ip_type, test_id", + [ + # Happy path + ( + "127.0.0.1", + 8080, + 4, + "valid_ipv4", + ), + ( + "2001:0db8:85a3:0000:0000:8a2e:0370:7334", + 3030, + 6, + "valid_ipv6", + ), + ], +) +def test_valid_ipv4_and_ipv6_address( + ip, port, expected_ip_type, test_id, mock_get_external_ip +): + # Arrange + hotkey = MockHotkey("5EemgxS7cmYbD34esCFoBgUZZC8JdnGtQvV5Qw3QFUCRRtGP") + coldkey = MockHotkey("5EemgxS7cmYbD34esCFoBgUZZC8JdnGtQvV5Qw3QFUCRRtGP") + coldkeypub = MockHotkey("5EemgxS7cmYbD34esCFoBgUZZC8JdnGtQvV5Qw3QFUCRRtGP") + wallet = MockWallet(hotkey, coldkey, coldkeypub) + + axon = Axon() + axon.ip = ip + axon.external_ip = ip + axon.port = port + axon.wallet = wallet + + # Act + ip_type = axon.info().ip_type + + # Assert + assert ip_type == expected_ip_type, f"Test ID: {test_id}" + + +@pytest.mark.parametrize( + "ip, port, expected_exception", + [ + ( + "This Is not a valid address", + 65534, + netaddr.core.AddrFormatError, + ), + ], + ids=["failed to detect a valid IP address from %r"], +) +def test_invalid_ip_address(ip, port, expected_exception): + # Assert + with pytest.raises(expected_exception): + Axon(ip=ip, external_ip=ip, port=port).info() + + +@pytest.mark.parametrize( + "ip, port, ss58_address, started, forward_fns, expected_str, test_id", + [ + # Happy path + ( + "127.0.0.1", + 8080, + "5G9RtsTbiYJYQYJzUfTCs...", + True, + {"fn1": None}, + "Axon(127.0.0.1, 8080, 5G9RtsTbiYJYQYJzUfTCs..., started, ['fn1'])", + "happy_path_started_with_forward_fn", + ), + ( + "192.168.1.1", + 3030, + "5HqUkGuo62b5...", + False, + {}, + "Axon(192.168.1.1, 3030, 5HqUkGuo62b5..., stopped, [])", + "happy_path_stopped_no_forward_fn", + ), + # Edge cases + ("", 0, "", False, {}, "Axon(, 0, , stopped, [])", "edge_empty_values"), + ( + "255.255.255.255", + 65535, + "5G9RtsTbiYJYQYJzUfTCs...", + True, + {"fn1": None, "fn2": None}, + "Axon(255.255.255.255, 65535, 5G9RtsTbiYJYQYJzUfTCs..., started, ['fn1', 'fn2'])", + "edge_max_values", + ), + ], +) +def test_axon_str_representation( + ip, + port, + ss58_address, + started, + forward_fns, + expected_str, + test_id, + mock_get_external_ip, +): + # Arrange + hotkey = MockHotkey(ss58_address) + wallet = MockWallet(hotkey) + axon = Axon() + axon.ip = ip + axon.port = port + axon.wallet = wallet + axon.started = started + axon.forward_fns = forward_fns + + # Act + result_dunder_str = axon.__str__() + result_dunder_repr = axon.__repr__() + + # Assert + assert result_dunder_str == expected_str, f"Test ID: {test_id}" + assert result_dunder_repr == expected_str, f"Test ID: {test_id}" + + +class TestAxonMiddleware(IsolatedAsyncioTestCase): + def setUp(self): + # Create a mock app + self.mock_app = MagicMock() + # Create a mock axon + self.mock_axon = MagicMock() + self.mock_axon.uuid = "1234" + self.mock_axon.forward_class_types = { + "request_name": Synapse, + } + self.mock_axon.wallet.hotkey.sign.return_value = bytes.fromhex("aabbccdd") + # Create an instance of AxonMiddleware + self.axon_middleware = AxonMiddleware(self.mock_app, self.mock_axon) + return self.axon_middleware + + @pytest.mark.asyncio + async def test_preprocess(self): + # Mock the request + request = MagicMock(spec=Request) + request.url.path = "/request_name" + request.client.port = "5000" + request.client.host = "192.168.0.1" + request.headers = {} + + synapse = await self.axon_middleware.preprocess(request) + + # Check if the preprocess function fills the axon information into the synapse + assert synapse.axon.version == version_as_int + assert synapse.axon.uuid == "1234" + assert synapse.axon.nonce is not None + assert synapse.axon.status_message is None + assert synapse.axon.status_code == 100 + assert synapse.axon.signature == "0xaabbccdd" + + # Check if the preprocess function fills the dendrite information into the synapse + assert synapse.dendrite.port == 5000 + assert synapse.dendrite.ip == "192.168.0.1" + + # Check if the preprocess function sets the request name correctly + assert synapse.name == "request_name" + + +class SynapseHTTPClient(TestClient): + def post_synapse(self, synapse: Synapse): + return self.post( + f"/{synapse.__class__.__name__}", + json=synapse.model_dump(), + headers={"computed_body_hash": synapse.body_hash}, + ) + + +@pytest.mark.asyncio +class TestAxonHTTPAPIResponses: + @pytest.fixture + def axon(self): + return Axon( + ip="192.0.2.1", + external_ip="192.0.2.1", + wallet=MockWallet(MockHotkey("A"), MockHotkey("B"), MockHotkey("PUB")), + ) + + @pytest.fixture + def no_verify_axon(self, axon): + axon.default_verify = self.no_verify_fn + return axon + + @pytest.fixture + def http_client(self, axon): + return SynapseHTTPClient(axon.app) + + async def no_verify_fn(self, synapse): + return + + class NonDeterministicHeaders(pydantic.BaseModel): + """ + Helper class to verify headers. + + Size headers are non-determistic as for example, header_size depends on non-deterministic + processing-time value. + """ + + bt_header_axon_process_time: float = pydantic.Field(gt=0, lt=30) + timeout: float = pydantic.Field(gt=0, lt=30) + header_size: int = pydantic.Field(None, gt=10, lt=400) + total_size: int = pydantic.Field(gt=100, lt=10000) + content_length: Optional[int] = pydantic.Field( + None, alias="content-length", gt=100, lt=10000 + ) + + def assert_headers(self, response, expected_headers): + expected_headers = { + "bt_header_axon_status_code": "200", + "bt_header_axon_status_message": "Success", + **expected_headers, + } + headers = dict(response.headers) + non_deterministic_headers_names = { + field.alias or field_name + for field_name, field in self.NonDeterministicHeaders.model_fields.items() + } + non_deterministic_headers = { + field: headers.pop(field, None) for field in non_deterministic_headers_names + } + assert headers == expected_headers + self.NonDeterministicHeaders.model_validate(non_deterministic_headers) + + async def test_unknown_path(self, http_client): + response = http_client.get("/no_such_path") + assert (response.status_code, response.json()) == ( + 404, + { + "message": "Synapse name 'no_such_path' not found. Available synapses ['Synapse']" + }, + ) + + async def test_ping__no_dendrite(self, http_client): + response = http_client.post_synapse(Synapse()) + assert (response.status_code, response.json()) == ( + 401, + { + "message": "Not Verified with error: No SS58 formatted address or public key provided." + }, + ) + + async def test_ping__without_verification(self, http_client, axon): + axon.verify_fns["Synapse"] = self.no_verify_fn + request_synapse = Synapse() + response = http_client.post_synapse(request_synapse) + assert response.status_code == 200 + response_synapse = Synapse(**response.json()) + assert response_synapse.axon.status_code == 200 + self.assert_headers( + response, + { + "computed_body_hash": "a7ffc6f8bf1ed76651c14756a061d662f580ff4de43b49fa82d80a4b80f8434a", + "content-type": "application/json", + "name": "Synapse", + }, + ) + + @pytest.fixture + def custom_synapse_cls(self): + class CustomSynapse(Synapse): + pass + + return CustomSynapse + + @pytest.fixture + def streaming_synapse_cls(self): + class CustomStreamingSynapse(StreamingSynapse): + async def process_streaming_response(self, response): + pass + + def extract_response_json(self, response) -> dict: + return {} + + return CustomStreamingSynapse + + async def test_synapse__explicitly_set_status_code( + self, http_client, axon, custom_synapse_cls, no_verify_axon + ): + error_message = "Essential resource for CustomSynapse not found" + + async def forward_fn(synapse: custom_synapse_cls): + synapse.axon.status_code = 404 + synapse.axon.status_message = error_message + return synapse + + axon.attach(forward_fn) + + response = http_client.post_synapse(custom_synapse_cls()) + assert response.status_code == 404 + response_synapse = custom_synapse_cls(**response.json()) + assert ( + response_synapse.axon.status_code, + response_synapse.axon.status_message, + ) == (404, error_message) + + async def test_synapse__exception_with_set_status_code( + self, http_client, axon, custom_synapse_cls, no_verify_axon + ): + error_message = "Conflicting request" + + async def forward_fn(synapse: custom_synapse_cls): + synapse.axon.status_code = 409 + raise RunException(message=error_message, synapse=synapse) + + axon.attach(forward_fn) + + response = http_client.post_synapse(custom_synapse_cls()) + assert response.status_code == 409 + assert response.json() == {"message": error_message} + + async def test_synapse__internal_error( + self, http_client, axon, custom_synapse_cls, no_verify_axon + ): + async def forward_fn(synapse: custom_synapse_cls): + raise ValueError("error with potentially sensitive information") + + axon.attach(forward_fn) + + response = http_client.post_synapse(custom_synapse_cls()) + assert response.status_code == 500 + response_data = response.json() + assert sorted(response_data.keys()) == ["message"] + assert re.match(r"Internal Server Error #[\da-f\-]+", response_data["message"]) + + +def test_allowed_nonce_window_ns(): + mock_synapse = SynapseMock() + current_time = time.time_ns() + allowed_window_ns = allowed_nonce_window_ns(current_time, mock_synapse.timeout) + expected_window_ns = ( + current_time - ALLOWED_DELTA - (mock_synapse.timeout * NANOSECONDS_IN_SECOND) + ) + assert allowed_window_ns < current_time, ( + "Allowed window should be less than the current time" + ) + assert allowed_window_ns == expected_window_ns, ( + f"Expected {expected_window_ns} but got {allowed_window_ns}" + ) + + +@pytest.mark.parametrize("nonce_offset_seconds", [1, 3, 5, 10]) +def test_nonce_diff_seconds(nonce_offset_seconds): + mock_synapse = SynapseMock() + current_time_ns = time.time_ns() + synapse_nonce = current_time_ns - (nonce_offset_seconds * NANOSECONDS_IN_SECOND) + diff_seconds, allowed_delta_seconds = calculate_diff_seconds( + current_time_ns, mock_synapse.timeout, synapse_nonce + ) + + expected_diff_seconds = nonce_offset_seconds # Because we subtracted nonce_offset_seconds from current_time_ns + expected_allowed_delta_seconds = ( + ALLOWED_DELTA + (mock_synapse.timeout * NANOSECONDS_IN_SECOND) + ) / NANOSECONDS_IN_SECOND + + assert diff_seconds == expected_diff_seconds, ( + f"Expected {expected_diff_seconds} but got {diff_seconds}" + ) + assert allowed_delta_seconds == expected_allowed_delta_seconds, ( + f"Expected {expected_allowed_delta_seconds} but got {allowed_delta_seconds}" + ) + + +# Mimicking axon default_verify nonce verification +# True: Nonce is fresh, False: Nonce is old +def is_nonce_within_allowed_window(synapse_nonce, allowed_window_ns): + return not (synapse_nonce <= allowed_window_ns) + + +# Test assuming synapse timeout is the default 12 seconds +@pytest.mark.parametrize( + "nonce_offset_seconds, expected_result", + [(1, True), (3, True), (5, True), (15, True), (18, False), (19, False)], +) +def test_nonce_within_allowed_window(nonce_offset_seconds, expected_result): + mock_synapse = SynapseMock() + current_time_ns = time.time_ns() + synapse_nonce = current_time_ns - (nonce_offset_seconds * NANOSECONDS_IN_SECOND) + allowed_window_ns = allowed_nonce_window_ns(current_time_ns, mock_synapse.timeout) + + result = is_nonce_within_allowed_window(synapse_nonce, allowed_window_ns) + + assert result == expected_result, f"Expected {expected_result} but got {result}" + + @pytest.mark.parametrize( + "forward_fn_return_annotation", + [ + None, + fastapi.Response, + StreamingSynapse, + ], + ) + async def test_streaming_synapse( + self, + http_client, + axon, + streaming_synapse_cls, + no_verify_axon, + forward_fn_return_annotation, + ): + tokens = [f"data{i}\n" for i in range(10)] + + async def streamer(send): + for token in tokens: + await send( + { + "type": "http.response.body", + "body": token.encode(), + "more_body": True, + } + ) + await send({"type": "http.response.body", "body": b"", "more_body": False}) + + async def forward_fn(synapse: streaming_synapse_cls): + return synapse.create_streaming_response(token_streamer=streamer) + + if forward_fn_return_annotation is not None: + forward_fn.__annotations__["return"] = forward_fn_return_annotation + + axon.attach(forward_fn) + + response = http_client.post_synapse(streaming_synapse_cls()) + assert (response.status_code, response.text) == (200, "".join(tokens)) + self.assert_headers( + response, + { + "content-type": "text/event-stream", + "name": "CustomStreamingSynapse", + "computed_body_hash": "a7ffc6f8bf1ed76651c14756a061d662f580ff4de43b49fa82d80a4b80f8434a", + }, + ) + + +@pytest.mark.asyncio +async def test_threaded_fastapi(): + server_started = threading.Event() + server_stopped = threading.Event() + + @contextlib.asynccontextmanager + async def lifespan(app): + server_started.set() + yield + server_stopped.set() + + app = fastapi.FastAPI( + lifespan=lifespan, + ) + app.get("/")(lambda: "Hello World") + + server = FastAPIThreadedServer( + uvicorn.Config(app, loop="none"), + ) + server.start() + + server_started.wait(3.0) + + async def wait_for_server(): + while not (server.started or server_stopped.is_set()): + await asyncio.sleep(1.0) + + await asyncio.wait_for(wait_for_server(), 7.0) + + assert server.is_running is True + + async with aiohttp.ClientSession( + base_url="http://127.0.0.1:8000", + ) as session: + async with session.get("/") as response: + assert await response.text() == '"Hello World"' + + server.stop() + + assert server.should_exit is True + + server_stopped.wait() + + with pytest.raises(aiohttp.ClientConnectorError): + await session.get("/") diff --git a/tests/unit_tests/test_chain_data.py b/tests/unit_tests/test_chain_data.py new file mode 100644 index 0000000000..b2274f4a4f --- /dev/null +++ b/tests/unit_tests/test_chain_data.py @@ -0,0 +1,361 @@ +import pytest +import torch + +from async_substrate_interface.utils import json +from bittensor.core.chain_data import AxonInfo + +RAOPERTAO = 10**18 + + +@pytest.mark.parametrize( + "ip, expected, test_case", + [ + ("0.0.0.0", False, "ID_is_serving_false"), + ("127.0.0.1", True, "ID_is_serving_true"), + ], +) +def test_is_serving(ip, expected, test_case): + # Arrange + axon_info = AxonInfo( + version=1, ip=ip, port=8080, ip_type=4, hotkey="", coldkey="cold" + ) + + # Act + result = axon_info.is_serving + + # Assert + assert result == expected, f"Test case: {test_case}" + + +@pytest.mark.parametrize( + "ip_type, ip, port, expected, test_case", + [ + (4, "127.0.0.1", 8080, "/ipv4/127.0.0.1:8080", "ID_ip_str_ipv4"), + (6, "::1", 8080, "/ipv6/::1:8080", "ID_ip_str_ipv6"), + ], +) +def test_ip_str(ip_type, ip, port, expected, test_case): + # Arrange + axon_info = AxonInfo( + version=1, ip=ip, port=port, ip_type=ip_type, hotkey="hot", coldkey="cold" + ) + + # Act + result = axon_info.ip_str() + + # Assert + assert result == expected, f"Test case: {test_case}" + + +@pytest.mark.parametrize( + "other, expected, test_case", + [ + (None, False, "ID_eq_none"), + ( + AxonInfo( + version=1, + ip="127.0.0.1", + port=8080, + ip_type=4, + hotkey="hot", + coldkey="cold", + ), + True, + "ID_eq_equal", + ), + ( + AxonInfo( + version=2, + ip="127.0.0.1", + port=8080, + ip_type=4, + hotkey="hot", + coldkey="cold", + ), + False, + "ID_eq_diff_version", + ), + ], +) +def test_eq(other, expected, test_case): + # Arrange + axon_info = AxonInfo( + version=1, ip="127.0.0.1", port=8080, ip_type=4, hotkey="hot", coldkey="cold" + ) + + # Act + result = axon_info == other + + # Assert + assert result == expected, f"Test case: {test_case}" + + +@pytest.mark.parametrize( + "axon_info, expected, test_case", + [ + ( + AxonInfo( + version=1, + ip="127.0.0.1", + port=8080, + ip_type=4, + hotkey="hot", + coldkey="cold", + ), + json.dumps( + { + "version": 1, + "ip": "127.0.0.1", + "port": 8080, + "ip_type": 4, + "hotkey": "hot", + "coldkey": "cold", + "protocol": 4, + "placeholder1": 0, + "placeholder2": 0, + } + ), + "ID_to_string", + ), + ], +) +def test_to_string(axon_info, expected, test_case): + # Act + result = axon_info.to_string() + + # Assert + assert result == expected, f"Test case: {test_case}" + + +# Test AxonInfo.from_string method +@pytest.mark.parametrize( + "string, expected, test_case", + [ + ( + '{"version": 1, "ip": "127.0.0.1", "port": 8080, "ip_type": 4, "hotkey": "hot", "coldkey": "cold"}', + AxonInfo( + version=1, + ip="127.0.0.1", + port=8080, + ip_type=4, + hotkey="hot", + coldkey="cold", + ), + "ID_from_string_valid", + ), + ("invalid_json", AxonInfo(0, "", 0, 0, "", ""), "ID_from_string_invalid_json"), + ], +) +def test_from_string(string, expected, test_case): + # Act + result = AxonInfo.from_string(string) + + # Assert + assert result == expected, f"Test case: {test_case}" + + +# Test AxonInfo.from_neuron_info method +@pytest.mark.parametrize( + "neuron_info, expected, test_case", + [ + ( + { + "axon_info": { + "version": 1, + "ip": 2130706433, + "port": 8080, + "ip_type": 4, + }, + "hotkey": "hot", + "coldkey": "cold", + }, + AxonInfo( + version=1, + ip="127.0.0.1", + port=8080, + ip_type=4, + hotkey="hot", + coldkey="cold", + ), + "ID_from_neuron_info", + ), + ], +) +def test_from_neuron_info(neuron_info, expected, test_case): + # Act + result = AxonInfo.from_neuron_info(neuron_info) + + # Assert + assert result == expected, f"Test case: {test_case}" + + +# Test AxonInfo.to_parameter_dict method +@pytest.mark.parametrize( + "axon_info, test_case", + [ + ( + AxonInfo( + version=1, + ip="127.0.0.1", + port=8080, + ip_type=4, + hotkey="hot", + coldkey="cold", + ), + "ID_to_parameter_dict", + ), + ], +) +def test_to_parameter_dict(axon_info, test_case): + # Act + result = axon_info.to_parameter_dict() + + # Assert + assert isinstance(result, dict) + for key, value in axon_info.__dict__.items(): + assert key in result + assert result[key] == value, f"Test case: {test_case}" + + +@pytest.mark.parametrize( + "axon_info, test_case", + [ + ( + AxonInfo( + version=1, + ip="127.0.0.1", + port=8080, + ip_type=4, + hotkey="hot", + coldkey="cold", + ), + "ID_to_parameter_dict", + ), + ], +) +def test_to_parameter_dict_torch( + axon_info, + test_case, + force_legacy_torch_compatible_api, +): + result = axon_info.to_parameter_dict() + + # Assert + assert isinstance(result, torch.nn.ParameterDict) + for key, value in axon_info.__dict__.items(): + assert key in result + assert result[key] == value, f"Test case: {test_case}" + + +@pytest.mark.parametrize( + "parameter_dict, expected, test_case", + [ + ( + { + "version": 1, + "ip": "127.0.0.1", + "port": 8080, + "ip_type": 4, + "hotkey": "hot", + "coldkey": "cold", + }, + AxonInfo( + version=1, + ip="127.0.0.1", + port=8080, + ip_type=4, + hotkey="hot", + coldkey="cold", + ), + "ID_from_parameter_dict", + ), + ], +) +def test_from_parameter_dict(parameter_dict, expected, test_case): + # Act + result = AxonInfo.from_parameter_dict(parameter_dict) + + # Assert + assert result == expected, f"Test case: {test_case}" + + +@pytest.mark.parametrize( + "parameter_dict, expected, test_case", + [ + ( + torch.nn.ParameterDict( + { + "version": 1, + "ip": "127.0.0.1", + "port": 8080, + "ip_type": 4, + "hotkey": "hot", + "coldkey": "cold", + } + ), + AxonInfo( + version=1, + ip="127.0.0.1", + port=8080, + ip_type=4, + hotkey="hot", + coldkey="cold", + ), + "ID_from_parameter_dict", + ), + ], +) +def test_from_parameter_dict_torch( + parameter_dict, expected, test_case, force_legacy_torch_compatible_api +): + # Act + result = AxonInfo.from_parameter_dict(parameter_dict) + + # Assert + assert result == expected, f"Test case: {test_case}" + + +def create_neuron_info_decoded( + hotkey, + coldkey, + stake, + weights, + bonds, + rank, + emission, + incentive, + consensus, + trust, + validator_trust, + dividends, + uid, + netuid, + active, + last_update, + validator_permit, + pruning_score, + prometheus_info, + axon_info, +): + return { + "hotkey": hotkey, + "coldkey": coldkey, + "stake": stake, + "weights": weights, + "bonds": bonds, + "rank": rank, + "emission": emission, + "incentive": incentive, + "consensus": consensus, + "trust": trust, + "validator_trust": validator_trust, + "dividends": dividends, + "uid": uid, + "netuid": netuid, + "active": active, + "last_update": last_update, + "validator_permit": validator_permit, + "pruning_score": pruning_score, + "prometheus_info": prometheus_info, + "axon_info": axon_info, + } diff --git a/tests/unit_tests/test_config.py b/tests/unit_tests/test_config.py new file mode 100644 index 0000000000..5c1c2a0edf --- /dev/null +++ b/tests/unit_tests/test_config.py @@ -0,0 +1,31 @@ +import bittensor +import argparse + + +def test_py_config_parsed_successfully_rust_wallet(): + """Verify that python based config object is successfully parsed with rust-based wallet object.""" + parser = argparse.ArgumentParser() + + bittensor.wallet.add_args(parser) + bittensor.subtensor.add_args(parser) + bittensor.axon.add_args(parser) + bittensor.logging.add_args(parser) + + config = bittensor.config(parser) + + # override config manually since we can't apply mocking to rust objects easily + config.wallet.name = "new_wallet_name" + config.wallet.hotkey = "new_hotkey" + config.wallet.path = "/some/not_default/path" + + # Pass in the whole bittensor config + wallet = bittensor.wallet(config=config) + assert wallet.name == config.wallet.name + assert wallet.hotkey_str == config.wallet.hotkey + assert wallet.path == config.wallet.path + + # Pass in only the btwallet's config + wallet_two = bittensor.wallet(config=config.wallet) + assert wallet_two.name == config.wallet.name + assert wallet_two.hotkey_str == config.wallet.hotkey + assert wallet_two.path == config.wallet.path diff --git a/tests/unit_tests/test_dendrite.py b/tests/unit_tests/test_dendrite.py new file mode 100644 index 0000000000..38b2f8929f --- /dev/null +++ b/tests/unit_tests/test_dendrite.py @@ -0,0 +1,406 @@ +import asyncio +import typing +from unittest.mock import MagicMock, Mock + +import aiohttp +from bittensor_wallet.mock import get_mock_wallet +import pytest + +from bittensor.core.axon import Axon +from bittensor.core.dendrite import ( + DENDRITE_ERROR_MAPPING, + DENDRITE_DEFAULT_ERROR, + Dendrite, +) +from bittensor.core.synapse import TerminalInfo +from tests.helpers import get_mock_wallet +from bittensor.core.synapse import Synapse +from bittensor.core.chain_data import AxonInfo + + +class SynapseDummy(Synapse): + input: int + output: typing.Optional[int] = None + + +def dummy(synapse: SynapseDummy) -> SynapseDummy: + synapse.output = synapse.input + 1 + return synapse + + +@pytest.fixture +def setup_dendrite(mock_get_external_ip): + # Assuming bittensor.Wallet() returns a wallet object + user_wallet = get_mock_wallet() + dendrite_obj = Dendrite(user_wallet) + yield dendrite_obj + + +@pytest.fixture +def axon_info(): + return AxonInfo( + version=1, + ip="127.0.0.1", + port=666, + ip_type=4, + hotkey="hot", + coldkey="cold", + ) + + +@pytest.fixture(scope="session") +def setup_axon(): + wallet = get_mock_wallet() + axon = Axon( + wallet, + external_ip="192.168.1.1", + ) + axon.attach(forward_fn=dummy) + axon.start() + yield axon + del axon + + +def test_init(setup_dendrite): + assert isinstance(setup_dendrite, Dendrite) + + +def test_str(setup_dendrite): + expected_string = f"dendrite({setup_dendrite.keypair.ss58_address})" + assert str(setup_dendrite) == expected_string + + +def test_repr(setup_dendrite): + expected_string = f"dendrite({setup_dendrite.keypair.ss58_address})" + assert repr(setup_dendrite) == expected_string + + +def test_close(setup_dendrite, setup_axon): + axon = setup_axon + # Query the axon to open a session + setup_dendrite.query(axon, SynapseDummy(input=1)) + # Session should be automatically closed after query + assert setup_dendrite._session is None + + +def test_garbage_collection(setup_dendrite): + del setup_dendrite # should not raise an error + + +@pytest.mark.asyncio +async def test_async_garbage_collection(setup_dendrite, setup_axon): + async with setup_dendrite as dendrite: + assert (await dendrite.session) is not None + del setup_dendrite # should not raise error + + +@pytest.mark.asyncio +async def test_aclose(setup_dendrite, setup_axon): + axon = setup_axon + # Use context manager to open an async session + async with setup_dendrite: + await setup_dendrite([axon], SynapseDummy(input=1), deserialize=False) + # Close should automatically be called on the session after context manager scope + assert setup_dendrite._session is None + + +class AsyncMock(Mock): + def __call__(self, *args, **kwargs): + sup = super(AsyncMock, self) + + async def coro(): + return sup.__call__(*args, **kwargs) + + return coro() + + def __await__(self): + return self().__await__() + + +def test_dendrite_create_wallet(mock_get_external_ip): + d = Dendrite(get_mock_wallet()) + d = Dendrite(get_mock_wallet().hotkey) + d = Dendrite(get_mock_wallet().coldkeypub) + assert d.__str__() == d.__repr__() + + +@pytest.mark.asyncio +async def test_forward_many(mock_get_external_ip): + n = 10 + d = Dendrite(wallet=get_mock_wallet()) + d.call = AsyncMock() + axons = [MagicMock() for _ in range(n)] + + resps = await d(axons) + assert len(resps) == n + resp = await d(axons[0]) + assert len([resp]) == 1 + + resps = await d.forward(axons) + assert len(resps) == n + resp = await d.forward(axons[0]) + assert len([resp]) == 1 + + +def test_pre_process_synapse(mock_get_external_ip): + d = Dendrite(wallet=get_mock_wallet()) + s = Synapse() + synapse = d.preprocess_synapse_for_request( + target_axon_info=Axon(wallet=get_mock_wallet()).info(), + synapse=s, + timeout=12, + ) + assert synapse.timeout == 12 + assert synapse.dendrite + assert synapse.axon + assert synapse.dendrite.ip + assert synapse.dendrite.version + assert synapse.dendrite.nonce + assert synapse.dendrite.uuid + assert synapse.dendrite.hotkey + assert synapse.axon.ip + assert synapse.axon.port + assert synapse.axon.hotkey + assert synapse.dendrite.signature + + +# Helper functions for casting, assuming they exist and work correctly. +def cast_int(value: typing.Any) -> int: + return int(value) + + +def cast_float(value: typing.Any) -> float: + return float(value) + + +# Happy path tests +@pytest.mark.parametrize( + "status_code, status_message, process_time, ip, port, version, nonce, uuid, hotkey, signature, expected", + [ + ( + 200, + "Success", + 0.1, + "198.123.23.1", + 9282, + 111, + 111111, + "5ecbd69c-1cec-11ee-b0dc-e29ce36fec1a", + "5EnjDGNqqWnuL2HCAdxeEtN2oqtXZw6BMBe936Kfy2PFz1J1", + "0x0813029319030129u4120u10841824y0182u091u230912u", + True, + ), + # Add more test cases with different combinations of realistic values + ], + ids=["basic-success"], +) +def test_terminal_info_happy_path( + status_code, + status_message, + process_time, + ip, + port, + version, + nonce, + uuid, + hotkey, + signature, + expected, +): + # Act + terminal_info = TerminalInfo( + status_code=status_code, + status_message=status_message, + process_time=process_time, + ip=ip, + port=port, + version=version, + nonce=nonce, + uuid=uuid, + hotkey=hotkey, + signature=signature, + ) + + # Assert + assert isinstance(terminal_info, TerminalInfo) == expected + assert terminal_info.status_code == status_code + assert terminal_info.status_message == status_message + assert terminal_info.process_time == process_time + assert terminal_info.ip == ip + assert terminal_info.port == port + assert terminal_info.version == version + assert terminal_info.nonce == nonce + assert terminal_info.uuid == uuid + assert terminal_info.hotkey == hotkey + assert terminal_info.signature == signature + + +# Edge cases +@pytest.mark.parametrize( + "status_code, process_time, port, version, nonce, expected_exception", + [ + ("not-an-int", 0.1, 9282, 111, 111111, ValueError), # status_code not an int + (200, "not-a-float", 9282, 111, 111111, ValueError), # process_time not a float + (200, 0.1, "not-an-int", 111, 111111, ValueError), # port not an int + # Add more edge cases as needed + ], + ids=["status_code-not-int", "process_time-not-float", "port-not-int"], +) +def test_terminal_info_edge_cases( + status_code, process_time, port, version, nonce, expected_exception +): + # Act & Assert + with pytest.raises(expected_exception): + TerminalInfo( + status_code=status_code, + process_time=process_time, + port=port, + version=version, + nonce=nonce, + ) + + +# Error case +@pytest.mark.parametrize( + "status_code, process_time, port, ip, version, nonce, expected_exception", + [ + (None, 0.1, 9282, 111, TerminalInfo(), 111111, TypeError), + ], + ids=[ + "int() argument must be a string, a bytes-like object or a real number, not 'TerminalInfo'" + ], +) +def test_terminal_info_error_cases( + status_code, process_time, port, ip, version, nonce, expected_exception +): + # Act & Assert + with pytest.raises(expected_exception): + TerminalInfo( + status_code=status_code, + process_time=process_time, + port=port, + ip=ip, + version=version, + nonce=nonce, + ) + + +@pytest.mark.asyncio +async def test_dendrite__call__success_response( + axon_info, setup_dendrite, mock_aio_response +): + input_synapse = SynapseDummy(input=1) + expected_synapse = SynapseDummy( + **( + input_synapse.model_dump() + | dict( + output=2, + axon=TerminalInfo( + status_code=200, + status_message="Success", + process_time=0.1, + ), + ) + ) + ) + mock_aio_response.post( + f"http://127.0.0.1:666/SynapseDummy", + body=expected_synapse.model_dump_json(), + ) + synapse = await setup_dendrite.call(axon_info, synapse=input_synapse) + + assert synapse.input == 1 + assert synapse.output == 2 + assert synapse.dendrite.status_code == 200 + assert synapse.dendrite.status_message == "Success" + assert synapse.dendrite.process_time >= 0 + + +@pytest.mark.asyncio +async def test_dendrite__call__handles_http_error_response( + axon_info, setup_dendrite, mock_aio_response +): + status_code = 414 + message = "Custom Error" + + mock_aio_response.post( + "http://127.0.0.1:666/SynapseDummy", + status=status_code, + payload={"message": message}, + ) + synapse = await setup_dendrite.call(axon_info, synapse=SynapseDummy(input=1)) + + assert synapse.axon.status_code == synapse.dendrite.status_code == status_code + assert synapse.axon.status_message == synapse.dendrite.status_message == message + + +@pytest.mark.parametrize( + "exception, expected_status_code, expected_message, synapse_timeout, synapse_ip, synapse_port, request_name", + [ + ( + aiohttp.ClientConnectorError(Mock(), Mock()), + DENDRITE_ERROR_MAPPING[aiohttp.ClientConnectorError][0], + f"{DENDRITE_ERROR_MAPPING[aiohttp.ClientConnectorError][1]} at 127.0.0.1:8080/test_request", + None, + "127.0.0.1", + "8080", + "test_request_client_connector_error", + ), + ( + asyncio.TimeoutError(), + DENDRITE_ERROR_MAPPING[asyncio.TimeoutError][0], + f"{DENDRITE_ERROR_MAPPING[asyncio.TimeoutError][1]} after 5 seconds", + 5, + None, + None, + "test_request_timeout", + ), + ( + aiohttp.ClientResponseError(Mock(), Mock(), status=404), + "404", + f"{DENDRITE_ERROR_MAPPING[aiohttp.ClientResponseError][1]}: 404, message=''", + None, + None, + None, + "test_request_client_response_error", + ), + ( + Exception("Unknown error"), + DENDRITE_DEFAULT_ERROR[0], + f"{DENDRITE_DEFAULT_ERROR[1]}: Unknown error", + None, + None, + None, + "test_request_unknown_error", + ), + ], + ids=[ + "ClientConnectorError", + "TimeoutError", + "ClientResponseError", + "GenericException", + ], +) +def test_process_error_message( + exception, + expected_status_code, + expected_message, + synapse_timeout, + synapse_ip, + synapse_port, + request_name, + setup_dendrite, +): + # Arrange + synapse = Mock() + + synapse.timeout = synapse_timeout + synapse.axon.ip = synapse_ip + synapse.axon.port = synapse_port + + # Act + result = setup_dendrite.process_error_message(synapse, request_name, exception) + + # Assert + assert result.dendrite.status_code == expected_status_code + assert expected_message in result.dendrite.status_message diff --git a/tests/unit_tests/test_deprecated.py b/tests/unit_tests/test_deprecated.py new file mode 100644 index 0000000000..f47337e019 --- /dev/null +++ b/tests/unit_tests/test_deprecated.py @@ -0,0 +1,34 @@ +import sys + + +def test_mock_import(): + """ + Tests that `bittensor.mock` can be imported and is the same as `bittensor.utils.mock`. + """ + import bittensor.mock as redirected_mock + import bittensor.utils.mock as real_mock + + assert "bittensor.mock" in sys.modules + assert redirected_mock is real_mock + + +def test_extrinsics_import(): + """Tests that `bittensor.extrinsics` can be imported and is the same as `bittensor.utils.deprecated.extrinsics`.""" + import bittensor.extrinsics as redirected_extrinsics + import bittensor.core.extrinsics as real_extrinsics + + assert "bittensor.extrinsics" in sys.modules + assert redirected_extrinsics is real_extrinsics + + +def test_object_aliases_are_correctly_mapped(): + """Ensures all object aliases correctly map to their respective classes in Bittensor package.""" + import bittensor + + assert issubclass(bittensor.axon, bittensor.Axon) + assert issubclass(bittensor.config, bittensor.Config) + assert issubclass(bittensor.dendrite, bittensor.Dendrite) + assert issubclass(bittensor.keyfile, bittensor.Keyfile) + assert issubclass(bittensor.metagraph, bittensor.Metagraph) + assert issubclass(bittensor.wallet, bittensor.Wallet) + assert issubclass(bittensor.synapse, bittensor.Synapse) diff --git a/tests/unit_tests/test_easy_imports.py b/tests/unit_tests/test_easy_imports.py new file mode 100644 index 0000000000..8ebd020faf --- /dev/null +++ b/tests/unit_tests/test_easy_imports.py @@ -0,0 +1,18 @@ +from bittensor.utils import easy_imports +import bittensor + +import pytest + + +@pytest.mark.parametrize( + "attr", + [ + a + for a in dir(easy_imports) + if ( + not a.startswith("__") and a not in ["sys", "importlib"] + ) # we don't care about systemwide pkgs + ], +) +def test_easy_imports(attr): + assert getattr(bittensor, attr), attr diff --git a/tests/unit_tests/test_errors.py b/tests/unit_tests/test_errors.py new file mode 100644 index 0000000000..8a556b22e4 --- /dev/null +++ b/tests/unit_tests/test_errors.py @@ -0,0 +1,49 @@ +from bittensor.core.errors import ( + ChainError, + HotKeyAccountNotExists, +) + + +def test_from_error(): + error = { + "type": "Module", + "name": "HotKeyAccountNotExists", + "docs": ["The hotkey does not exists"], + } + + exception = ChainError.from_error(error) + + assert isinstance(exception, HotKeyAccountNotExists) + assert exception.args[0] == "The hotkey does not exists" + + +def test_from_error_unsupported_exception(): + error = { + "type": "Module", + "name": "UnknownException", + "docs": ["Unknown"], + } + + exception = ChainError.from_error(error) + + assert isinstance(exception, ChainError) + assert exception.args[0] == error + + +def test_from_error_new_exception(): + error = { + "type": "Module", + "name": "NewException", + "docs": ["New"], + } + + exception = ChainError.from_error(error) + + assert isinstance(exception, ChainError) + + class NewException(ChainError): + pass + + exception = ChainError.from_error(error) + + assert isinstance(exception, NewException) diff --git a/tests/unit_tests/test_logging.py b/tests/unit_tests/test_logging.py new file mode 100644 index 0000000000..f84bfd9341 --- /dev/null +++ b/tests/unit_tests/test_logging.py @@ -0,0 +1,236 @@ +import logging as stdlogging +import multiprocessing +from unittest.mock import MagicMock, patch + +import pytest + +from bittensor.utils.btlogging import LoggingMachine +from bittensor.utils.btlogging.defines import ( + DEFAULT_LOG_FILE_NAME, + BITTENSOR_LOGGER_NAME, +) +from bittensor.utils.btlogging.loggingmachine import LoggingConfig, _concat_message + + +@pytest.fixture(autouse=True, scope="session") +def disable_stdout_streaming(): + # Backup original handlers + original_handlers = stdlogging.root.handlers[:] + + # Remove all handlers that stream to stdout + stdlogging.root.handlers = [ + h + for h in stdlogging.root.handlers + if not isinstance(h, stdlogging.StreamHandler) + ] + + yield # Yield control to the test or fixture setup + + # Restore original handlers after the test + stdlogging.root.handlers = original_handlers + + +@pytest.fixture +def mock_config(tmp_path): + # Using pytest's tmp_path fixture to generate a temporary directory + log_dir = tmp_path / "logs" + log_dir.mkdir() # Create the temporary directory + log_file_path = log_dir / DEFAULT_LOG_FILE_NAME + + mock_config = LoggingConfig( + debug=False, trace=False, info=False, record_log=True, logging_dir=str(log_dir) + ) + + yield mock_config, log_file_path + # Cleanup: No need to explicitly delete the log file or directory, tmp_path does it automatically + + +@pytest.fixture +def logging_machine(mock_config): + config, _ = mock_config + logging_machine = LoggingMachine(config=config) + return logging_machine + + +def test_initialization(logging_machine, mock_config): + """ + Test initialization of LoggingMachine. + """ + config, log_file_path = mock_config # Unpack to get the log_file_path + + assert logging_machine.get_queue() is not None + assert isinstance(logging_machine.get_queue(), multiprocessing.queues.Queue) + assert logging_machine.get_config() == config + + # Ensure that handlers are set up correctly + assert any( + isinstance(handler, stdlogging.StreamHandler) + for handler in logging_machine._handlers + ) + if config.record_log and config.logging_dir: + assert any( + isinstance(handler, stdlogging.FileHandler) + for handler in logging_machine._handlers + ) + assert log_file_path.exists() # Check if log file is created + + +def test_state_transitions(logging_machine, mock_config): + """ + Test state transitions and the associated logging level changes. + """ + config, log_file_path = mock_config + with patch( + "bittensor.utils.btlogging.loggingmachine.all_loggers" + ) as mocked_all_loggers: + # mock the main bittensor logger, identified by its `name` field + mocked_bt_logger = MagicMock() + mocked_bt_logger.name = BITTENSOR_LOGGER_NAME + # third party loggers are treated differently and silenced under default + # logging settings + mocked_third_party_logger = MagicMock() + logging_machine._logger = mocked_bt_logger + mocked_all_loggers.return_value = [mocked_third_party_logger, mocked_bt_logger] + + # Enable/Disable Debug + # from default + assert logging_machine.current_state_value == "Default" + logging_machine.enable_debug() + assert logging_machine.current_state_value == "Debug" + # check log levels + mocked_bt_logger.setLevel.assert_called_with(stdlogging.DEBUG) + mocked_third_party_logger.setLevel.assert_called_with(stdlogging.DEBUG) + + logging_machine.disable_debug() + + # Enable/Disable Trace + assert logging_machine.current_state_value == "Default" + logging_machine.enable_trace() + assert logging_machine.current_state_value == "Trace" + # check log levels + mocked_bt_logger.setLevel.assert_called_with(stdlogging.TRACE) + mocked_third_party_logger.setLevel.assert_called_with(stdlogging.TRACE) + logging_machine.disable_trace() + assert logging_machine.current_state_value == "Default" + + # Enable Default + logging_machine.enable_debug() + assert logging_machine.current_state_value == "Debug" + logging_machine.enable_default() + assert logging_machine.current_state_value == "Default" + # main logger set to INFO + mocked_bt_logger.setLevel.assert_called_with(stdlogging.WARNING) + # 3rd party loggers should be disabled by setting to CRITICAL + mocked_third_party_logger.setLevel.assert_called_with(stdlogging.CRITICAL) + + # Disable Logging + # from default + logging_machine.disable_logging() + assert logging_machine.current_state_value == "Disabled" + mocked_bt_logger.setLevel.assert_called_with(stdlogging.CRITICAL) + mocked_third_party_logger.setLevel.assert_called_with(stdlogging.CRITICAL) + + +def test_enable_file_logging_with_new_config(tmp_path): + """ + Test enabling file logging by setting a new config. + """ + log_dir = tmp_path / "logs" + log_dir.mkdir() # Create the temporary directory + log_file_path = log_dir / DEFAULT_LOG_FILE_NAME + + # check no file handler is created + config = LoggingConfig( + debug=False, trace=False, info=False, record_log=True, logging_dir=None + ) + lm = LoggingMachine(config) + assert not any( + isinstance(handler, stdlogging.FileHandler) for handler in lm._handlers + ) + + # check file handler now exists + new_config = LoggingConfig( + debug=False, trace=False, info=False, record_log=True, logging_dir=str(log_dir) + ) + lm.set_config(new_config) + assert any(isinstance(handler, stdlogging.FileHandler) for handler in lm._handlers) + + +def test_all_log_levels_output(logging_machine, caplog): + """ + Test that all log levels are captured. + """ + logging_machine.set_trace() + + logging_machine.trace("Test trace") + logging_machine.debug("Test debug") + logging_machine.info("Test info") + logging_machine.success("Test success") + logging_machine.warning("Test warning") + logging_machine.error("Test error") + logging_machine.critical("Test critical") + + assert "Test trace" in caplog.text + assert "Test debug" in caplog.text + assert "Test info" in caplog.text + assert "Test success" in caplog.text + assert "Test warning" in caplog.text + assert "Test error" in caplog.text + assert "Test critical" in caplog.text + + records = [(r.module, r.getMessage()) for r in caplog.records] + + assert records == [ + ("loggingmachine", "Trace enabled."), + ("test_logging", "Test trace"), + ("test_logging", "Test debug"), + ("test_logging", "Test info"), + ("test_logging", "Test success"), + ("test_logging", "Test warning"), + ("test_logging", "Test error"), + ("test_logging", "Test critical"), + ] + + +@pytest.mark.parametrize( + "msg, prefix, suffix, expected_result", + [ + ("msg", "", "", "msg"), + ("msg", None, None, "msg"), + ("msg", "prefix", None, "prefix - msg"), + ("msg", None, "suffix", "msg - suffix"), + ("msg", "prefix", "suffix", "prefix - msg - suffix"), + ], + ids=[ + "message, no prefix (str), no suffix (str)", + "message, no prefix (None), no suffix (None)", + "message and prefix only", + "message and suffix only", + "message, prefix, and suffix", + ], +) +def test_concat(msg, prefix, suffix, expected_result): + """Test different options of message concatenation with prefix and suffix.""" + assert _concat_message(msg, prefix, suffix) == expected_result + + +def test_logger_level(logging_machine, caplog): + """Test get/set Logger level.""" + + assert logging_machine.get_level() == stdlogging.WARN + + logging_machine.info("info1") + logging_machine.warning("warn1") + + assert "info1" not in caplog.text + assert "warn1" in caplog.text + + logging_machine.setLevel(stdlogging.INFO) + + assert logging_machine.get_level() == stdlogging.INFO + + logging_machine.info("info2") + logging_machine.warning("warn2") + + assert "info2" in caplog.text + assert "warn2" in caplog.text diff --git a/tests/unit_tests/test_metagraph.py b/tests/unit_tests/test_metagraph.py new file mode 100644 index 0000000000..cdb735f627 --- /dev/null +++ b/tests/unit_tests/test_metagraph.py @@ -0,0 +1,263 @@ +import asyncio +import copy +from bittensor.utils.balance import Balance +from unittest.mock import Mock + +import numpy as np +import pytest + +from bittensor.core import settings +from bittensor.core.metagraph import Metagraph +from bittensor.core.subtensor import Subtensor + + +@pytest.fixture +def mock_environment(mocker): + # Create a Mock for subtensor + subtensor = mocker.AsyncMock() + + # Create a list of Mock Neurons + neurons = [ + Mock( + uid=i, + trust=i + 0.5, + consensus=i + 0.1, + incentive=i + 0.2, + dividends=i + 0.3, + rank=i + 0.4, + emission=i + 0.5, + active=i, + last_update=i, + validator_permit=i % 2 == 0, + validator_trust=i + 0.6, + total_stake=Mock(tao=i + 0.7), + stake=Balance.from_tao(i) + Balance.from_tao(0.8), + axon_info=f"axon_info_{i}", + weights=[(j, j + 0.1) for j in range(5)], + bonds=[(j, j + 0.2) for j in range(5)], + ) + for i in range(10) + ] + + return subtensor, neurons + + +@pytest.mark.asyncio +async def test_set_metagraph_attributes(mock_environment): + subtensor, neurons = mock_environment + metagraph = Metagraph(1, sync=False) + metagraph.neurons = neurons + metagraph._set_metagraph_attributes(block=5) + + # Check the attributes are set as expected + assert metagraph.n.item() == len(neurons) + assert metagraph.block.item() == 5 + assert ( + np.array_equal( + metagraph.uids, + np.array([neuron.uid for neuron in neurons], dtype=np.int64), + ) + is True + ) + + assert ( + np.array_equal( + metagraph.trust, + np.array([neuron.trust for neuron in neurons], dtype=np.float32), + ) + is True + ) + + assert ( + np.array_equal( + metagraph.consensus, + np.array([neuron.consensus for neuron in neurons], dtype=np.float32), + ) + is True + ) + # Similarly for other attributes... + + # Test the axons + assert metagraph.axons == [n.axon_info for n in neurons] + + +def test_process_weights_or_bonds(mock_environment): + _, neurons = mock_environment + metagraph = Metagraph(1, sync=False) + metagraph.neurons = neurons + + # Test weights processing + weights = metagraph._process_weights_or_bonds( + data=[neuron.weights for neuron in neurons], attribute="weights" + ) + assert weights.shape[0] == len( + neurons + ) # Number of rows should be equal to number of neurons + assert weights.shape[1] == len( + neurons + ) # Number of columns should be equal to number of neurons + # TODO: Add more checks to ensure the weights have been processed correctly + + # Test bonds processing + bonds = metagraph._process_weights_or_bonds( + data=[neuron.bonds for neuron in neurons], attribute="bonds" + ) + assert bonds.shape[0] == len( + neurons + ) # Number of rows should be equal to number of neurons + assert bonds.shape[1] == len( + neurons + ) # Number of columns should be equal to number of neurons + + # TODO: Add more checks to ensure the bonds have been processed correctly + + +# Mocking the bittensor.Subtensor class for testing purposes +@pytest.fixture +def mock_subtensor(mocker): + subtensor = mocker.Mock(spec=Subtensor) + subtensor.chain_endpoint = settings.FINNEY_ENTRYPOINT + subtensor.network = "finney" + subtensor.async_subtensor = mocker.AsyncMock( + get_current_block=mocker.AsyncMock(return_value=601) + ) + subtensor.event_loop = asyncio.new_event_loop() + return subtensor + + +# Mocking the metagraph instance for testing purposes +@pytest.fixture +def metagraph_instance(mocker): + metagraph = Metagraph(netuid=1337, sync=False) + metagraph._assign_neurons = mocker.Mock() + metagraph._set_metagraph_attributes = mocker.Mock() + metagraph._set_weights_and_bonds = mocker.Mock() + metagraph._get_all_stakes_from_chain = mocker.Mock() + return metagraph + + +@pytest.fixture +def loguru_sink(): + class LogSink: + def __init__(self): + self.messages = [] + + def write(self, message): + # Assuming `message` is an object, you might need to adjust how you extract the text + self.messages.append(str(message)) + + def __contains__(self, item): + return any(item in message for message in self.messages) + + return LogSink() + + +@pytest.mark.parametrize( + "block, test_id", + [ + (300, "warning_case_block_greater_than_300"), + ], +) +def test_sync_warning_cases(block, test_id, metagraph_instance, mock_subtensor, caplog): + mock_subtensor.get_current_block.return_value = 601 + mock_subtensor.get_metagraph_info.return_value = [] + metagraph_instance.sync(block=block, lite=True, subtensor=mock_subtensor) + + expected_message = "Attempting to sync longer than 300 blocks ago on a non-archive node. Please use the 'archive' network for subtensor and retry." + assert expected_message in caplog.text, ( + f"Test ID: {test_id} - Expected warning message not found in Loguru sink." + ) + + +def test_deepcopy(mock_environment): + subtensor, neurons = mock_environment + metagraph = Metagraph(1, sync=False) + metagraph.neurons = neurons + metagraph.subtensor = subtensor + + # Do a deep copy + copied_metagraph = copy.deepcopy(metagraph) + + # Check that the subtensor attribute is None + assert copied_metagraph.subtensor is None + + # Check that other attributes are copied correctly + assert copied_metagraph.n == metagraph.n + assert copied_metagraph.block == metagraph.block + assert np.array_equal(copied_metagraph.uids, metagraph.uids) + assert np.array_equal(copied_metagraph.stake, metagraph.stake) + assert np.array_equal(copied_metagraph.total_stake, metagraph.total_stake) + assert np.array_equal(copied_metagraph.ranks, metagraph.ranks) + assert np.array_equal(copied_metagraph.trust, metagraph.trust) + assert np.array_equal(copied_metagraph.consensus, metagraph.consensus) + assert np.array_equal(copied_metagraph.validator_trust, metagraph.validator_trust) + assert np.array_equal(copied_metagraph.incentive, metagraph.incentive) + assert np.array_equal(copied_metagraph.emission, metagraph.emission) + assert np.array_equal(copied_metagraph.dividends, metagraph.dividends) + assert np.array_equal(copied_metagraph.active, metagraph.active) + assert np.array_equal(copied_metagraph.last_update, metagraph.last_update) + assert np.array_equal(copied_metagraph.validator_permit, metagraph.validator_permit) + assert np.array_equal(copied_metagraph.weights, metagraph.weights) + assert np.array_equal(copied_metagraph.bonds, metagraph.bonds) + + # Check that the neurons are different objects in the original and copied metagraphs + for original_neuron, copied_neuron in zip( + metagraph.neurons, copied_metagraph.neurons + ): + assert original_neuron is not copied_neuron + assert original_neuron.uid == copied_neuron.uid + assert original_neuron.trust == copied_neuron.trust + assert original_neuron.consensus == copied_neuron.consensus + assert original_neuron.incentive == copied_neuron.incentive + assert original_neuron.dividends == copied_neuron.dividends + assert original_neuron.rank == copied_neuron.rank + assert original_neuron.emission == copied_neuron.emission + assert original_neuron.active == copied_neuron.active + assert original_neuron.last_update == copied_neuron.last_update + assert original_neuron.validator_permit == copied_neuron.validator_permit + assert original_neuron.validator_trust == copied_neuron.validator_trust + assert original_neuron.total_stake.tao == copied_neuron.total_stake.tao + assert original_neuron.stake == copied_neuron.stake + assert original_neuron.axon_info == copied_neuron.axon_info + assert original_neuron.weights == copied_neuron.weights + assert original_neuron.bonds == copied_neuron.bonds + + +def test_copy(mock_environment): + subtensor, neurons = mock_environment + metagraph = Metagraph(1, sync=False) + metagraph.neurons = neurons + metagraph.subtensor = subtensor + + # Do a shallow copy + copied_metagraph = copy.copy(metagraph) + + # Check that the subtensor attribute is None in the copied object + assert copied_metagraph.subtensor is None + + # Check that other attributes are copied correctly + assert copied_metagraph.n == metagraph.n + assert copied_metagraph.block == metagraph.block + assert np.array_equal(copied_metagraph.uids, metagraph.uids) + assert np.array_equal(copied_metagraph.stake, metagraph.stake) + assert np.array_equal(copied_metagraph.total_stake, metagraph.total_stake) + assert np.array_equal(copied_metagraph.ranks, metagraph.ranks) + assert np.array_equal(copied_metagraph.trust, metagraph.trust) + assert np.array_equal(copied_metagraph.consensus, metagraph.consensus) + assert np.array_equal(copied_metagraph.validator_trust, metagraph.validator_trust) + assert np.array_equal(copied_metagraph.incentive, metagraph.incentive) + assert np.array_equal(copied_metagraph.emission, metagraph.emission) + assert np.array_equal(copied_metagraph.dividends, metagraph.dividends) + assert np.array_equal(copied_metagraph.active, metagraph.active) + assert np.array_equal(copied_metagraph.last_update, metagraph.last_update) + assert np.array_equal(copied_metagraph.validator_permit, metagraph.validator_permit) + assert copied_metagraph.axons == metagraph.axons + assert copied_metagraph.neurons == metagraph.neurons + assert np.array_equal(copied_metagraph.weights, metagraph.weights) + assert np.array_equal(copied_metagraph.bonds, metagraph.bonds) + + # Check that the neurons are the same objects in the original and copied metagraphs + for original_neuron, copied_neuron in zip( + metagraph.neurons, copied_metagraph.neurons + ): + assert original_neuron is copied_neuron diff --git a/tests/unit_tests/test_subnets.py b/tests/unit_tests/test_subnets.py new file mode 100644 index 0000000000..00a5276264 --- /dev/null +++ b/tests/unit_tests/test_subnets.py @@ -0,0 +1,78 @@ +import pytest + +from bittensor.utils import subnets + + +class MySubnetsAPI(subnets.SubnetsAPI): + """Example of user class inherited from SubnetsAPI.""" + + def prepare_synapse(self, *args, **kwargs): + """Prepare the synapse-specific payload.""" + + def process_responses(self, responses): + """Process the responses from the network.""" + return responses + + +def test_instance_creation(fake_wallet, mocker): + """Test the creation of a MySubnetsAPI instance.""" + # Prep + mocked_dendrite = mocker.patch.object(subnets, "Dendrite") + + # Call + instance = MySubnetsAPI(fake_wallet) + + # Asserts + assert isinstance(instance, subnets.SubnetsAPI) + mocked_dendrite.assert_called_once_with(wallet=fake_wallet) + assert instance.dendrite == mocked_dendrite.return_value + assert instance.wallet == fake_wallet + + +@pytest.mark.asyncio +async def test_query_api(fake_wallet, mocker): + """Test querying the MySubnetsAPI instance asynchronously.""" + # Prep + mocked_async_dendrite = mocker.AsyncMock() + mocked_dendrite = mocker.patch.object( + subnets, "Dendrite", return_value=mocked_async_dendrite + ) + + fake_axon = mocker.MagicMock() + + mocked_synapse = mocker.MagicMock() + mocked_synapse.return_value.name = "test synapse" + mocked_prepare_synapse = mocker.patch.object( + MySubnetsAPI, "prepare_synapse", return_value=mocked_synapse + ) + + # Call + instance = MySubnetsAPI(fake_wallet) + result = await instance.query_api(fake_axon, **{"key": "val"}) + + # Asserts + mocked_prepare_synapse.assert_called_once_with(key="val") + mocked_dendrite.assert_called_once_with(wallet=fake_wallet) + assert result == mocked_async_dendrite.return_value + + +@pytest.mark.asyncio +async def test_test_instance_call(fake_wallet, mocker): + """Test the MySubnetsAPI instance call with asynchronous handling.""" + # Prep + mocked_async_dendrite = mocker.AsyncMock() + mocked_dendrite = mocker.patch.object( + subnets, "Dendrite", return_value=mocked_async_dendrite + ) + mocked_query_api = mocker.patch.object( + MySubnetsAPI, "query_api", new=mocker.AsyncMock() + ) + fake_axon = mocker.MagicMock() + + # Call + instance = MySubnetsAPI(fake_wallet) + await instance(fake_axon) + + # Asserts + mocked_dendrite.assert_called_once_with(wallet=fake_wallet) + mocked_query_api.assert_called_once_with(fake_axon) diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py new file mode 100644 index 0000000000..a8882e9897 --- /dev/null +++ b/tests/unit_tests/test_subtensor.py @@ -0,0 +1,4246 @@ +import argparse +import unittest.mock as mock +import datetime +from unittest.mock import MagicMock + +import pytest +from bittensor_wallet import Wallet +from async_substrate_interface import sync_substrate +from async_substrate_interface.types import ScaleObj +import websockets + +from bittensor import StakeInfo +from bittensor.core import settings +from bittensor.core import subtensor as subtensor_module +from bittensor.core.async_subtensor import AsyncSubtensor, logging +from bittensor.core.axon import Axon +from bittensor.core.chain_data import SubnetHyperparameters, SelectiveMetagraphIndex +from bittensor.core.settings import version_as_int +from bittensor.core.subtensor import Subtensor +from bittensor.core.types import AxonServeCallParams +from bittensor.utils import ( + Certificate, + u16_normalized_float, + u64_normalized_float, + determine_chain_endpoint_and_network, +) +from bittensor.utils.balance import Balance + +U16_MAX = 65535 +U64_MAX = 18446744073709551615 + + +@pytest.fixture +def fake_call_params(): + return call_params() + + +def call_params(): + return AxonServeCallParams( + version=settings.version_as_int, + ip=0, + port=9090, + ip_type=4, + netuid=1, + hotkey="str", + coldkey="str", + protocol=4, + placeholder1=0, + placeholder2=0, + certificate=None, + ) + + +def call_params_with_certificate(): + params = call_params() + params.certificate = Certificate("fake_cert") + return params + + +def test_methods_comparable(mock_substrate): + """Verifies that methods in sync and async Subtensors are comparable.""" + # Preps + subtensor = Subtensor(_mock=True) + async_subtensor = AsyncSubtensor(_mock=True) + + # methods which lives in async subtensor only + excluded_async_subtensor_methods = ["initialize"] + subtensor_methods = [m for m in dir(subtensor) if not m.startswith("_")] + + async_subtensor_methods = [ + m + for m in dir(async_subtensor) + if not m.startswith("_") and m not in excluded_async_subtensor_methods + ] + + # Assertions + for method in subtensor_methods: + assert method in async_subtensor_methods, ( + f"`Subtensor.{method}` not in `AsyncSubtensor` class." + ) + + for method in async_subtensor_methods: + assert method in subtensor_methods, ( + f"`AsyncSubtensor.{method}` not in `Subtensor` class." + ) + + +def test_serve_axon_with_external_ip_set(): + internal_ip: str = "192.0.2.146" + external_ip: str = "2001:0db8:85a3:0000:0000:8a2e:0370:7334" + + mock_serve_axon = MagicMock(return_value=True) + + mock_subtensor = MagicMock(spec=Subtensor, serve_axon=mock_serve_axon) + + mock_wallet = MagicMock( + spec=Wallet, + coldkey=MagicMock(), + coldkeypub=MagicMock( + # mock ss58 address + ss58_address="5DD26kC2kxajmwfbbZmVmxhrY9VeeyR1Gpzy9i8wxLUg6zxm" + ), + hotkey=MagicMock( + ss58_address="5CtstubuSoVLJGCXkiWRNKrrGg2DVBZ9qMs2qYTLsZR4q1Wg" + ), + ) + + mock_config = Axon.config() + mock_axon_with_external_ip_set = Axon( + wallet=mock_wallet, + ip=internal_ip, + external_ip=external_ip, + config=mock_config, + ) + + mock_subtensor.serve_axon( + netuid=-1, + axon=mock_axon_with_external_ip_set, + ) + + mock_serve_axon.assert_called_once() + + # verify that the axon is served to the network with the external ip + _, kwargs = mock_serve_axon.call_args + axon_info = kwargs["axon"].info() + assert axon_info.ip == external_ip + + +def test_serve_axon_with_external_port_set(mock_get_external_ip): + internal_port: int = 1234 + external_port: int = 5678 + + mock_serve = MagicMock(return_value=True) + + mock_serve_axon = MagicMock(return_value=True) + + mock_subtensor = MagicMock( + spec=Subtensor, + serve=mock_serve, + serve_axon=mock_serve_axon, + ) + + mock_wallet = MagicMock( + spec=Wallet, + coldkey=MagicMock(), + coldkeypub=MagicMock( + # mock ss58 address + ss58_address="5DD26kC2kxajmwfbbZmVmxhrY9VeeyR1Gpzy9i8wxLUg6zxm" + ), + hotkey=MagicMock( + ss58_address="5CtstubuSoVLJGCXkiWRNKrrGg2DVBZ9qMs2qYTLsZR4q1Wg" + ), + ) + + mock_config = Axon.config() + + mock_axon_with_external_port_set = Axon( + wallet=mock_wallet, + port=internal_port, + external_port=external_port, + config=mock_config, + ) + + mock_subtensor.serve_axon( + netuid=-1, + axon=mock_axon_with_external_port_set, + ) + + mock_serve_axon.assert_called_once() + # verify that the axon is served to the network with the external port + _, kwargs = mock_serve_axon.call_args + axon_info = kwargs["axon"].info() + assert axon_info.port == external_port + + +class ExitEarly(Exception): + """Mock exception to exit early from the called code""" + + pass + + +@pytest.mark.parametrize( + "test_id, expected_output", + [ + # Happy path test + ( + "happy_path_default", + "Create and return a new object. See help(type) for accurate signature.", + ), + ], +) +def test_help(test_id, expected_output, capsys): + # Act + Subtensor.help() + + # Assert + captured = capsys.readouterr() + assert expected_output in captured.out, f"Test case {test_id} failed" + + +@pytest.fixture +def parser(): + return argparse.ArgumentParser() + + +# Mocking argparse.ArgumentParser.add_argument method to simulate ArgumentError +def test_argument_error_handling(monkeypatch, parser): + def mock_add_argument(*args, **kwargs): + raise argparse.ArgumentError(None, "message") + + monkeypatch.setattr(argparse.ArgumentParser, "add_argument", mock_add_argument) + # No exception should be raised + Subtensor.add_args(parser) + + +@pytest.mark.parametrize( + "network, expected_network, expected_endpoint", + [ + # Happy path tests + ("finney", "finney", settings.FINNEY_ENTRYPOINT), + ("local", "local", settings.LOCAL_ENTRYPOINT), + ("test", "test", settings.FINNEY_TEST_ENTRYPOINT), + ("archive", "archive", settings.ARCHIVE_ENTRYPOINT), + # Endpoint override tests + ( + settings.FINNEY_ENTRYPOINT, + "finney", + settings.FINNEY_ENTRYPOINT, + ), + ( + "entrypoint-finney.opentensor.ai", + "finney", + settings.FINNEY_ENTRYPOINT, + ), + ( + settings.FINNEY_TEST_ENTRYPOINT, + "test", + settings.FINNEY_TEST_ENTRYPOINT, + ), + ( + "test.finney.opentensor.ai", + "test", + settings.FINNEY_TEST_ENTRYPOINT, + ), + ( + settings.ARCHIVE_ENTRYPOINT, + "archive", + settings.ARCHIVE_ENTRYPOINT, + ), + ( + "archive.chain.opentensor.ai", + "archive", + settings.ARCHIVE_ENTRYPOINT, + ), + ("127.0.0.1", "local", "127.0.0.1"), + ("localhost", "local", "localhost"), + ("ws://127.0.0.1:9945", "local", "ws://127.0.0.1:9945"), + ("ws://localhost:9945", "local", "ws://localhost:9945"), + # Edge cases + (None, None, None), + ("unknown", "unknown", "unknown"), + ], +) +def test_determine_chain_endpoint_and_network( + network, expected_network, expected_endpoint +): + # Act + result_network, result_endpoint = determine_chain_endpoint_and_network(network) + + # Assert + assert result_network == expected_network + assert result_endpoint == expected_endpoint + + +@pytest.fixture +def mock_logger(): + with mock.patch.object(logging, "warning") as mock_warning: + yield mock_warning + + +def test_hyperparameter_subnet_does_not_exist(subtensor, mocker): + """Tests when the subnet does not exist.""" + subtensor.subnet_exists = mocker.MagicMock(return_value=False) + assert subtensor.get_hyperparameter("Difficulty", 1, None) is None + subtensor.subnet_exists.assert_called_once_with(1, block=None) + + +def test_hyperparameter_result_is_none(subtensor, mocker): + """Tests when query_subtensor returns None.""" + subtensor.subnet_exists = mocker.MagicMock(return_value=True) + subtensor.substrate.query = mocker.MagicMock(return_value=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( + module="SubtensorModule", + storage_function="Difficulty", + params=[1], + block_hash=None, + ) + + +def test_hyperparameter_result_has_no_value(subtensor, mocker): + """Test when the result has no 'value' attribute.""" + subtensor.subnet_exists = mocker.MagicMock(return_value=True) + subtensor.substrate.query = mocker.MagicMock(return_value=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( + module="SubtensorModule", + storage_function="Difficulty", + params=[1], + block_hash=None, + ) + + +def test_hyperparameter_success_int(subtensor, mocker): + """Test when query_subtensor returns an integer value.""" + subtensor.subnet_exists = mocker.MagicMock(return_value=True) + subtensor.substrate.query = mocker.MagicMock( + return_value=mocker.MagicMock(value=100) + ) + assert subtensor.get_hyperparameter("Difficulty", 1, None) == 100 + subtensor.subnet_exists.assert_called_once_with(1, block=None) + subtensor.substrate.query.assert_called_once_with( + module="SubtensorModule", + storage_function="Difficulty", + params=[1], + block_hash=None, + ) + + +def test_hyperparameter_success_float(subtensor, mocker): + """Test when query_subtensor returns a float value.""" + subtensor.subnet_exists = mocker.MagicMock(return_value=True) + subtensor.substrate.query = mocker.MagicMock( + return_value=mocker.MagicMock(value=0.5) + ) + assert subtensor.get_hyperparameter("Difficulty", 1, None) == 0.5 + subtensor.subnet_exists.assert_called_once_with(1, block=None) + subtensor.substrate.query.assert_called_once_with( + module="SubtensorModule", + storage_function="Difficulty", + params=[1], + block_hash=None, + ) + + +def test_blocks_since_last_update_success_calls(subtensor, mocker): + """Tests the weights_rate_limit method to ensure it correctly fetches the LastUpdate hyperparameter.""" + # Prep + uid = 7 + mocked_current_block = 2 + mocked_result = {uid: 1} + mocked_get_hyperparameter = mocker.patch.object( + subtensor, + "get_hyperparameter", + return_value=mocked_result, + ) + mocked_get_current_block = mocker.patch.object( + subtensor, + "get_current_block", + return_value=mocked_current_block, + ) + + # Call + result = subtensor.blocks_since_last_update(netuid=7, uid=uid) + + # Assertions + mocked_get_current_block.assert_called_once() + mocked_get_hyperparameter.assert_called_once_with(param_name="LastUpdate", netuid=7) + assert result == 1 + # if we change the methods logic in the future we have to be make sure the returned type is correct + assert isinstance(result, int) + + +def test_weights_rate_limit_success_calls(subtensor, mocker): + """Tests the weights_rate_limit method to ensure it correctly fetches the WeightsSetRateLimit hyperparameter.""" + # Prep + mocked_get_hyperparameter = mocker.patch.object( + subtensor, + "get_hyperparameter", + return_value=5, + ) + + # Call + result = subtensor.weights_rate_limit(netuid=7) + + # Assertions + mocked_get_hyperparameter.assert_called_once_with( + param_name="WeightsSetRateLimit", + netuid=7, + block=None, + ) + # if we change the methods logic in the future we have to be make sure the returned type is correct + assert isinstance(result, int) + + +@pytest.fixture +def sample_hyperparameters(): + return MagicMock(spec=SubnetHyperparameters) + + +def normalize_hyperparameters( + subnet: "SubnetHyperparameters", +) -> list[tuple[str, str, str]]: + """ + Normalizes the hyperparameters of a subnet. + + Args: + subnet: The subnet hyperparameters object. + + Returns: + A list of tuples containing the parameter name, value, and normalized value. + """ + param_mappings = { + "adjustment_alpha": u64_normalized_float, + "min_difficulty": u64_normalized_float, + "max_difficulty": u64_normalized_float, + "difficulty": u64_normalized_float, + "bonds_moving_avg": u64_normalized_float, + "max_weight_limit": u16_normalized_float, + "kappa": u16_normalized_float, + "alpha_high": u16_normalized_float, + "alpha_low": u16_normalized_float, + "min_burn": Balance.from_rao, + "max_burn": Balance.from_rao, + } + + normalized_values: list[tuple[str, str, str]] = [] + subnet_dict = subnet.__dict__ + + for param, value in subnet_dict.items(): + try: + if param in param_mappings: + norm_value = param_mappings[param](value) + if isinstance(norm_value, float): + norm_value = f"{norm_value:.{10}g}" + else: + norm_value = value + except Exception as e: + logging.console.error(f"❌ Error normalizing parameter '{param}': {e}") + norm_value = "-" + + normalized_values.append((param, str(value), str(norm_value))) + + return normalized_values + + +def get_normalized_value(normalized_data, param_name): + return next( + ( + norm_value + for p_name, _, norm_value in normalized_data + if p_name == param_name + ), + None, + ) + + +@pytest.mark.parametrize( + "param_name, max_value, mid_value, zero_value, is_balance", + [ + ("adjustment_alpha", U64_MAX, U64_MAX / 2, 0, False), + ("max_weight_limit", U16_MAX, U16_MAX / 2, 0, False), + ("difficulty", U64_MAX, U64_MAX / 2, 0, False), + ("min_difficulty", U64_MAX, U64_MAX / 2, 0, False), + ("max_difficulty", U64_MAX, U64_MAX / 2, 0, False), + ("bonds_moving_avg", U64_MAX, U64_MAX / 2, 0, False), + ("min_burn", 10000000000, 5000000000, 0, True), # These are in rao + ("max_burn", 20000000000, 10000000000, 0, True), + ], + ids=[ + "adjustment-alpha", + "max_weight_limit", + "difficulty", + "min_difficulty", + "max_difficulty", + "bonds_moving_avg", + "min_burn", + "max_burn", + ], +) +def test_hyperparameter_normalization( + sample_hyperparameters, param_name, max_value, mid_value, zero_value, is_balance +): + setattr(sample_hyperparameters, param_name, mid_value) + normalized = normalize_hyperparameters(sample_hyperparameters) + norm_value = get_normalized_value(normalized, param_name) + + # Mid-value test + if is_balance: + numeric_value = float(str(norm_value).lstrip(settings.TAO_SYMBOL)) + expected_tao = mid_value / 1e9 + assert numeric_value == expected_tao, ( + f"Mismatch in tao value for {param_name} at mid value" + ) + else: + assert float(norm_value) == 0.5, f"Failed mid-point test for {param_name}" + + # Max-value test + setattr(sample_hyperparameters, param_name, max_value) + normalized = normalize_hyperparameters(sample_hyperparameters) + norm_value = get_normalized_value(normalized, param_name) + + if is_balance: + numeric_value = float(str(norm_value).lstrip(settings.TAO_SYMBOL)) + expected_tao = max_value / 1e9 + assert numeric_value == expected_tao, ( + f"Mismatch in tao value for {param_name} at max value" + ) + else: + assert float(norm_value) == 1.0, f"Failed max value test for {param_name}" + + # Zero-value test + setattr(sample_hyperparameters, param_name, zero_value) + normalized = normalize_hyperparameters(sample_hyperparameters) + norm_value = get_normalized_value(normalized, param_name) + + if is_balance: + numeric_value = float(str(norm_value).lstrip(settings.TAO_SYMBOL)) + expected_tao = zero_value / 1e9 + assert numeric_value == expected_tao, ( + f"Mismatch in tao value for {param_name} at zero value" + ) + else: + assert float(norm_value) == 0.0, f"Failed zero value test for {param_name}" + + +########################### +# Account functions tests # +########################### + + +def test_commit_reveal_enabled(subtensor, mocker): + """Test commit_reveal_enabled.""" + # Preps + netuid = 1 + block = 123 + mocked_get_hyperparameter = mocker.patch.object(subtensor, "get_hyperparameter") + + # Call + result = subtensor.commit_reveal_enabled(netuid, block) + + # Assertions + mocked_get_hyperparameter.assert_called_once_with( + param_name="CommitRevealWeightsEnabled", block=block, netuid=netuid + ) + assert result is False + + +def test_get_subnet_reveal_period_epochs(subtensor, mocker): + """Test get_subnet_reveal_period_epochs.""" + # Preps + netuid = 1 + block = 123 + mocked_get_hyperparameter = mocker.patch.object(subtensor, "get_hyperparameter") + + # Call + result = subtensor.get_subnet_reveal_period_epochs(netuid, block) + + # Assertions + mocked_get_hyperparameter.assert_called_once_with( + param_name="RevealPeriodEpochs", block=block, netuid=netuid + ) + assert result == mocked_get_hyperparameter.return_value + + +########################### +# Global Parameters tests # +########################### + + +# `block` property test +def test_block_property(mocker, subtensor): + """Test block property returns the correct block number.""" + expected_block = 123 + mocker.patch.object(subtensor, "get_current_block", return_value=expected_block) + + result = subtensor.block + + assert result == expected_block + subtensor.get_current_block.assert_called_once() + + +# `subnet_exists` tests +def test_subnet_exists_success(mocker, subtensor): + """Test subnet_exists returns True when subnet exists.""" + # Prep + netuid = 1 + block = 123 + mock_result = mocker.MagicMock(value=True) + mocker.patch.object(subtensor.substrate, "query", return_value=mock_result) + + # Call + result = subtensor.subnet_exists(netuid, block) + + # Asserts + assert result is True + subtensor.substrate.query.assert_called_once_with( + module="SubtensorModule", + storage_function="NetworksAdded", + params=[netuid], + block_hash=subtensor.substrate.get_block_hash.return_value, + ) + subtensor.substrate.get_block_hash.assert_called_once_with(block) + + +def test_subnet_exists_no_data(mocker, subtensor): + """Test subnet_exists returns False when no subnet information is found.""" + # Prep + netuid = 1 + block = 123 + mocker.patch.object(subtensor.substrate, "query", return_value=None) + + # Call + result = subtensor.subnet_exists(netuid, block) + + # Asserts + assert result is False + subtensor.substrate.query.assert_called_once_with( + module="SubtensorModule", + storage_function="NetworksAdded", + params=[netuid], + block_hash=subtensor.substrate.get_block_hash.return_value, + ) + subtensor.substrate.get_block_hash.assert_called_once_with(block) + + +def test_subnet_exists_no_value_attribute(mocker, subtensor): + """Test subnet_exists returns False when result has no value attribute.""" + # Prep + netuid = 1 + block = 123 + mock_result = mocker.MagicMock() + del mock_result.value + mocker.patch.object(subtensor.substrate, "query", return_value=mock_result) + + # Call + result = subtensor.subnet_exists(netuid, block) + + # Asserts + assert result is False + subtensor.substrate.query.assert_called_once_with( + module="SubtensorModule", + storage_function="NetworksAdded", + params=[netuid], + block_hash=subtensor.substrate.get_block_hash.return_value, + ) + subtensor.substrate.get_block_hash.assert_called_once_with(block) + + +def test_subnet_exists_no_block(mocker, subtensor): + """Test subnet_exists with no block specified.""" + # Prep + netuid = 1 + mock_result = mocker.MagicMock(value=True) + mocker.patch.object(subtensor.substrate, "query", return_value=mock_result) + + # Call + result = subtensor.subnet_exists(netuid) + + # Asserts + assert result is True + subtensor.substrate.query.assert_called_once_with( + module="SubtensorModule", + storage_function="NetworksAdded", + params=[netuid], + block_hash=None, + ) + subtensor.substrate.get_block_hash.assert_not_called() + + +# `get_total_subnets` tests +def test_get_total_subnets_success(mocker, subtensor): + """Test get_total_subnets returns correct data when total subnet information is found.""" + # Prep + block = 123 + total_subnets_value = 10 + mock_result = mocker.MagicMock(value=total_subnets_value) + mocker.patch.object(subtensor.substrate, "query", return_value=mock_result) + + # Call + result = subtensor.get_total_subnets(block) + + # Asserts + assert result is not None + assert result == total_subnets_value + subtensor.substrate.query.assert_called_once_with( + module="SubtensorModule", + storage_function="TotalNetworks", + params=[], + block_hash=subtensor.substrate.get_block_hash.return_value, + ) + subtensor.substrate.get_block_hash.assert_called_once_with(block) + + +def test_get_total_subnets_no_data(mocker, subtensor): + """Test get_total_subnets returns None when no total subnet information is found.""" + # Prep + block = 123 + mocker.patch.object(subtensor.substrate, "query", return_value=None) + + # Call + result = subtensor.get_total_subnets(block) + + # Asserts + assert result is None + subtensor.substrate.query.assert_called_once_with( + module="SubtensorModule", + storage_function="TotalNetworks", + params=[], + block_hash=subtensor.substrate.get_block_hash.return_value, + ) + subtensor.substrate.get_block_hash.assert_called_once_with(block) + + +def test_get_total_subnets_no_value_attribute(mocker, subtensor): + """Test get_total_subnets returns None when result has no value attribute.""" + # Prep + block = 123 + mock_result = mocker.MagicMock() + del mock_result.value # Simulating a missing value attribute + mocker.patch.object(subtensor.substrate, "query", return_value=mock_result) + + # Call + result = subtensor.get_total_subnets(block) + + # Asserts + assert result is None + subtensor.substrate.query.assert_called_once_with( + module="SubtensorModule", + storage_function="TotalNetworks", + params=[], + block_hash=subtensor.substrate.get_block_hash.return_value, + ) + subtensor.substrate.get_block_hash.assert_called_once_with(block) + + +def test_get_total_subnets_no_block(mocker, subtensor): + """Test get_total_subnets with no block specified.""" + # Prep + total_subnets_value = 10 + mock_result = mocker.MagicMock(value=total_subnets_value) + mocker.patch.object(subtensor.substrate, "query", return_value=mock_result) + + # Call + result = subtensor.get_total_subnets() + + # Asserts + assert result is not None + assert result == total_subnets_value + subtensor.substrate.query.assert_called_once_with( + module="SubtensorModule", + storage_function="TotalNetworks", + params=[], + block_hash=None, + ) + subtensor.substrate.get_block_hash.assert_not_called() + + +# `get_subnets` tests +def test_get_subnets_success(mocker, subtensor): + """Test get_subnets returns correct list when subnet information is found.""" + # Prep + block = 123 + mock_result = mocker.MagicMock() + mock_result.records = [(1, True), (2, True)] + mock_result.__iter__.return_value = iter(mock_result.records) + mocker.patch.object(subtensor.substrate, "query_map", return_value=mock_result) + + # Call + result = subtensor.get_subnets(block) + + # Asserts + assert result == [1, 2] + subtensor.substrate.query_map.assert_called_once_with( + module="SubtensorModule", + storage_function="NetworksAdded", + block_hash=subtensor.substrate.get_block_hash.return_value, + ) + subtensor.substrate.get_block_hash.assert_called_once_with(block) + + +def test_get_subnets_no_data(mocker, subtensor): + """Test get_subnets returns empty list when no subnet information is found.""" + # Prep + block = 123 + mock_result = mocker.MagicMock() + mock_result.records = [] + mocker.patch.object(subtensor.substrate, "query_map", return_value=mock_result) + + # Call + result = subtensor.get_subnets(block) + + # Asserts + assert result == [] + subtensor.substrate.query_map.assert_called_once_with( + module="SubtensorModule", + storage_function="NetworksAdded", + block_hash=subtensor.substrate.get_block_hash.return_value, + ) + subtensor.substrate.get_block_hash.assert_called_once_with(block) + + +def test_get_subnets_no_block_specified(mocker, subtensor): + """Test get_subnets with no block specified.""" + # Prep + mock_result = mocker.MagicMock() + mock_result.records = [(1, True), (2, True)] + mock_result.__iter__.return_value = iter(mock_result.records) + mocker.patch.object(subtensor.substrate, "query_map", return_value=mock_result) + + # Call + result = subtensor.get_subnets() + + # Asserts + assert result == [1, 2] + subtensor.substrate.query_map.assert_called_once_with( + module="SubtensorModule", + storage_function="NetworksAdded", + block_hash=None, + ) + subtensor.substrate.get_block_hash.assert_not_called + + +# `get_subnet_hyperparameters` tests +def test_get_subnet_hyperparameters_success(mocker, subtensor): + """Test get_subnet_hyperparameters returns correct data when hyperparameters are found.""" + # Prep + netuid = 1 + block = 123 + + mocker.patch.object( + subtensor, + "query_runtime_api", + ) + mocker.patch.object( + subtensor_module.SubnetHyperparameters, + "from_dict", + ) + + # Call + subtensor.get_subnet_hyperparameters(netuid, block) + + # Asserts + subtensor.query_runtime_api.assert_called_once_with( + runtime_api="SubnetInfoRuntimeApi", + method="get_subnet_hyperparams_v2", + params=[netuid], + block=block, + ) + subtensor_module.SubnetHyperparameters.from_dict.assert_called_once_with( + subtensor.query_runtime_api.return_value, + ) + + +def test_get_subnet_hyperparameters_no_data(mocker, subtensor): + """Test get_subnet_hyperparameters returns empty list when no data is found.""" + # Prep + netuid = 1 + block = 123 + mocker.patch.object(subtensor, "query_runtime_api", return_value=None) + mocker.patch.object(subtensor_module.SubnetHyperparameters, "from_dict") + + # Call + result = subtensor.get_subnet_hyperparameters(netuid, block) + + # Asserts + assert result is None + subtensor.query_runtime_api.assert_called_once_with( + runtime_api="SubnetInfoRuntimeApi", + method="get_subnet_hyperparams_v2", + params=[netuid], + block=block, + ) + subtensor_module.SubnetHyperparameters.from_dict.assert_not_called() + + +def test_query_subtensor(subtensor, mocker): + """Tests query_subtensor call.""" + # Prep + fake_name = "module_name" + + # Call + result = subtensor.query_subtensor(fake_name) + + # Asserts + subtensor.substrate.query.assert_called_once_with( + module="SubtensorModule", + storage_function=fake_name, + params=None, + block_hash=None, + ) + assert result == subtensor.substrate.query.return_value + + +def test_query_runtime_api(subtensor, mocker): + """Tests query_runtime_api call.""" + # Prep + fake_runtime_api = "NeuronInfoRuntimeApi" + fake_method = "get_neuron_lite" + + mock_determine_block_hash = mocker.patch.object( + 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) + + # Asserts + subtensor.substrate.runtime_call.assert_called_once_with( + fake_runtime_api, + fake_method, + None, + mock_determine_block_hash.return_value, + ) + mock_determine_block_hash.assert_called_once_with(None) + + assert result == subtensor.substrate.runtime_call.return_value.value + + +def test_query_map_subtensor(subtensor, mocker): + """Tests query_map_subtensor call.""" + # Prep + fake_name = "module_name" + + # Call + result = subtensor.query_map_subtensor(fake_name) + + # Asserts + subtensor.substrate.query_map.assert_called_once_with( + module="SubtensorModule", + storage_function=fake_name, + params=None, + block_hash=None, + ) + assert result == subtensor.substrate.query_map.return_value + + +def test_state_call(subtensor, mocker): + """Tests state_call call.""" + # Prep + fake_method = "method" + fake_data = "data" + + # Call + result = subtensor.state_call(fake_method, fake_data) + + # Asserts + subtensor.substrate.rpc_request.assert_called_once_with( + method="state_call", params=[fake_method, fake_data], block_hash=None + ) + assert result == subtensor.substrate.rpc_request.return_value + + +def test_query_map(subtensor, mocker): + """Tests query_map call.""" + # Prep + fake_module_name = "module_name" + fake_name = "constant_name" + + # Call + result = subtensor.query_map(fake_module_name, fake_name) + + # Asserts + subtensor.substrate.query_map.assert_called_once_with( + module=fake_module_name, + storage_function=fake_name, + params=None, + block_hash=None, + ) + assert result == subtensor.substrate.query_map.return_value + + +def test_query_constant(subtensor, mocker): + """Tests query_constant call.""" + # Prep + fake_module_name = "module_name" + fake_constant_name = "constant_name" + + # Call + result = subtensor.query_constant(fake_module_name, fake_constant_name) + + # Asserts + subtensor.substrate.get_constant.assert_called_once_with( + module_name=fake_module_name, + constant_name=fake_constant_name, + block_hash=None, + ) + assert result == subtensor.substrate.get_constant.return_value + + +def test_query_module(subtensor): + # Prep + fake_module = "module" + fake_name = "function_name" + + # Call + result = subtensor.query_module(fake_module, fake_name) + + # Asserts + subtensor.substrate.query.assert_called_once_with( + module=fake_module, + storage_function=fake_name, + params=None, + block_hash=None, + ) + assert result == subtensor.substrate.query.return_value + + +def test_metagraph(subtensor, mocker): + """Tests subtensor.metagraph call.""" + # Prep + fake_netuid = 1 + fake_lite = True + mocked_metagraph = mocker.patch.object(subtensor_module, "Metagraph") + + # Call + result = subtensor.metagraph(fake_netuid, fake_lite) + + # Asserts + mocked_metagraph.assert_called_once_with( + network=subtensor.chain_endpoint, + netuid=fake_netuid, + lite=fake_lite, + sync=False, + subtensor=subtensor, + ) + mocked_metagraph.return_value.sync.assert_called_once_with( + block=None, lite=fake_lite, subtensor=subtensor + ) + assert result == mocked_metagraph.return_value + + +def test_get_netuids_for_hotkey(subtensor, mocker): + """Tests get_netuids_for_hotkey call.""" + # Prep + fake_hotkey_ss58 = "hotkey_ss58" + fake_block = 123 + + mocked_query_map_subtensor = mocker.MagicMock() + mocker.patch.object(subtensor.substrate, "query_map", mocked_query_map_subtensor) + + # Call + result = subtensor.get_netuids_for_hotkey(fake_hotkey_ss58, fake_block) + + # Asserts + mocked_query_map_subtensor.assert_called_once_with( + module="SubtensorModule", + storage_function="IsNetworkMember", + 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 == [] + + +def test_get_current_block(subtensor): + """Tests get_current_block call.""" + # Call + result = subtensor.get_current_block() + + # Asserts + subtensor.substrate.get_block_number.assert_called_once_with(None) + assert result == subtensor.substrate.get_block_number.return_value + + +def test_is_hotkey_registered_any(subtensor, mocker): + """Tests is_hotkey_registered_any call""" + # Prep + fake_hotkey_ss58 = "hotkey_ss58" + fake_block = 123 + return_value = [1, 2] + + mocked_get_netuids_for_hotkey = mocker.MagicMock(return_value=return_value) + subtensor.get_netuids_for_hotkey = mocked_get_netuids_for_hotkey + + # Call + result = subtensor.is_hotkey_registered_any(fake_hotkey_ss58, fake_block) + + # Asserts + mocked_get_netuids_for_hotkey.assert_called_once_with(fake_hotkey_ss58, fake_block) + assert result is (len(return_value) > 0) + + +def test_is_hotkey_registered_on_subnet(subtensor, mocker): + """Tests is_hotkey_registered_on_subnet call.""" + # Prep + fake_hotkey_ss58 = "hotkey_ss58" + fake_netuid = 1 + fake_block = 123 + + mocked_get_uid_for_hotkey_on_subnet = mocker.MagicMock() + subtensor.get_uid_for_hotkey_on_subnet = mocked_get_uid_for_hotkey_on_subnet + + # Call + result = subtensor.is_hotkey_registered_on_subnet( + fake_hotkey_ss58, fake_netuid, fake_block + ) + + # Asserts + mocked_get_uid_for_hotkey_on_subnet.assert_called_once_with( + fake_hotkey_ss58, fake_netuid, block=fake_block + ) + assert result is (mocked_get_uid_for_hotkey_on_subnet.return_value is not None) + + +def test_is_hotkey_registered_without_netuid(subtensor, mocker): + """Tests is_hotkey_registered call with no netuid specified.""" + # Prep + fake_hotkey_ss58 = "hotkey_ss58" + + mocked_is_hotkey_registered_any = mocker.MagicMock() + subtensor.is_hotkey_registered_any = mocked_is_hotkey_registered_any + + # Call + + result = subtensor.is_hotkey_registered(fake_hotkey_ss58) + + # Asserts + mocked_is_hotkey_registered_any.assert_called_once_with(fake_hotkey_ss58, None) + assert result == mocked_is_hotkey_registered_any.return_value + + +def test_is_hotkey_registered_with_netuid(subtensor, mocker): + """Tests is_hotkey_registered call with netuid specified.""" + # Prep + fake_hotkey_ss58 = "hotkey_ss58" + fake_netuid = 123 + + mocked_is_hotkey_registered_on_subnet = mocker.MagicMock() + subtensor.is_hotkey_registered_on_subnet = mocked_is_hotkey_registered_on_subnet + + # Call + + result = subtensor.is_hotkey_registered(fake_hotkey_ss58, fake_netuid) + + # Asserts + mocked_is_hotkey_registered_on_subnet.assert_called_once_with( + fake_hotkey_ss58, fake_netuid, None + ) + assert result == mocked_is_hotkey_registered_on_subnet.return_value + + +def test_set_weights(subtensor, mocker, fake_wallet): + """Successful set_weights call.""" + # Preps + fake_netuid = 1 + fake_uids = [2, 4] + fake_weights = [0.4, 0.6] + fake_wait_for_inclusion = False + fake_wait_for_finalization = False + fake_max_retries = 5 + + expected_result = (True, None) + + mocked_get_uid_for_hotkey_on_subnet = mocker.MagicMock() + subtensor.get_uid_for_hotkey_on_subnet = mocked_get_uid_for_hotkey_on_subnet + + mocked_blocks_since_last_update = mocker.MagicMock(return_value=2) + subtensor.blocks_since_last_update = mocked_blocks_since_last_update + + mocked_weights_rate_limit = mocker.MagicMock(return_value=1) + subtensor.weights_rate_limit = mocked_weights_rate_limit + + mocked_set_weights_extrinsic = mocker.patch.object( + subtensor_module, "set_weights_extrinsic", return_value=expected_result + ) + + # Call + result = subtensor.set_weights( + wallet=fake_wallet, + netuid=fake_netuid, + uids=fake_uids, + weights=fake_weights, + version_key=settings.version_as_int, + wait_for_inclusion=fake_wait_for_inclusion, + wait_for_finalization=fake_wait_for_finalization, + max_retries=fake_max_retries, + ) + + # Asserts + mocked_get_uid_for_hotkey_on_subnet.assert_called_once_with( + fake_wallet.hotkey.ss58_address, fake_netuid + ) + mocked_blocks_since_last_update.assert_called_with( + fake_netuid, mocked_get_uid_for_hotkey_on_subnet.return_value + ) + mocked_weights_rate_limit.assert_called_with(fake_netuid) + mocked_set_weights_extrinsic.assert_called_with( + subtensor=subtensor, + wallet=fake_wallet, + netuid=fake_netuid, + uids=fake_uids, + weights=fake_weights, + version_key=settings.version_as_int, + wait_for_inclusion=fake_wait_for_inclusion, + wait_for_finalization=fake_wait_for_finalization, + period=8, + raise_error=True, + ) + assert result == expected_result + + +def test_serve_axon(subtensor, mocker): + """Tests successful serve_axon call.""" + # Prep + fake_netuid = 123 + fake_axon = mocker.MagicMock() + fake_wait_for_inclusion = False + fake_wait_for_finalization = True + fake_certificate = None + + mocked_serve_axon_extrinsic = mocker.patch.object( + subtensor_module, "serve_axon_extrinsic" + ) + + # Call + result = subtensor.serve_axon( + netuid=fake_netuid, + axon=fake_axon, + wait_for_inclusion=fake_wait_for_inclusion, + wait_for_finalization=fake_wait_for_finalization, + ) + + # Asserts + mocked_serve_axon_extrinsic.assert_called_once_with( + subtensor=subtensor, + netuid=fake_netuid, + axon=fake_axon, + certificate=fake_certificate, + period=None, + raise_error=False, + wait_for_inclusion=fake_wait_for_inclusion, + wait_for_finalization=fake_wait_for_finalization, + ) + assert result == mocked_serve_axon_extrinsic.return_value + + +def test_get_block_hash(subtensor, mocker): + """Tests successful get_block_hash call.""" + # Prep + fake_block_id = 123 + + # Call + result = subtensor.get_block_hash(fake_block_id) + + # Asserts + subtensor.substrate.get_block_hash.assert_called_once_with(fake_block_id) + assert result == subtensor.substrate.get_block_hash.return_value + + +def test_commit(subtensor, fake_wallet, mocker): + """Test a successful commit call.""" + # Preps + fake_netuid = 1 + fake_data = "some data to network" + mocked_publish_metadata = mocker.patch.object(subtensor_module, "publish_metadata") + + # Call + result = subtensor.set_commitment(fake_wallet, fake_netuid, fake_data) + + # Asserts + mocked_publish_metadata.assert_called_once_with( + subtensor=subtensor, + wallet=fake_wallet, + netuid=fake_netuid, + data_type=f"Raw{len(fake_data)}", + data=fake_data.encode(), + period=None, + raise_error=False, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + assert result is mocked_publish_metadata.return_value + + +def test_subnetwork_n(subtensor, mocker): + """Test successful subnetwork_n call.""" + # Prep + fake_netuid = 1 + fake_block = 123 + fake_result = 2 + + mocked_get_hyperparameter = mocker.patch.object( + subtensor, + "get_hyperparameter", + return_value=fake_result, + ) + + # Call + result = subtensor.subnetwork_n(fake_netuid, fake_block) + + # Asserts + mocked_get_hyperparameter.assert_called_once_with( + param_name="SubnetworkN", netuid=fake_netuid, block=fake_block + ) + assert result == mocked_get_hyperparameter.return_value + + +def test_transfer(subtensor, fake_wallet, mocker): + """Tests successful transfer call.""" + # Prep + fake_dest = "SS58PUBLICKEY" + fake_amount = 1.1 + fake_wait_for_inclusion = True + fake_wait_for_finalization = True + mocked_transfer_extrinsic = mocker.patch.object( + subtensor_module, "transfer_extrinsic" + ) + + # Call + result = subtensor.transfer( + wallet=fake_wallet, + destination=fake_dest, + amount=fake_amount, + wait_for_inclusion=fake_wait_for_inclusion, + wait_for_finalization=fake_wait_for_finalization, + ) + + # Asserts + mocked_transfer_extrinsic.assert_called_once_with( + subtensor=subtensor, + wallet=fake_wallet, + destination=fake_dest, + amount=Balance(fake_amount), + transfer_all=False, + wait_for_inclusion=fake_wait_for_inclusion, + wait_for_finalization=fake_wait_for_finalization, + keep_alive=True, + period=None, + raise_error=False, + ) + assert result == mocked_transfer_extrinsic.return_value + + +def test_get_neuron_for_pubkey_and_subnet(subtensor, mocker): + """Successful call to get_neuron_for_pubkey_and_subnet.""" + # Prep + fake_hotkey_ss58 = "fake_hotkey" + fake_netuid = 1 + fake_block = 123 + + mocker.patch.object( + subtensor.substrate, + "rpc_request", + return_value=mocker.MagicMock( + **{ + "get.return_value": "0x32", + }, + ), + ) + mock_neuron_from_dict = mocker.patch.object( + subtensor_module.NeuronInfo, + "from_dict", + return_value=["delegate1", "delegate2"], + ) + + # Call + result = subtensor.get_neuron_for_pubkey_and_subnet( + hotkey_ss58=fake_hotkey_ss58, + netuid=fake_netuid, + block=fake_block, + ) + + # Asserts + subtensor.substrate.query.assert_called_once_with( + module="SubtensorModule", + storage_function="Uids", + params=[fake_netuid, fake_hotkey_ss58], + block_hash=subtensor.substrate.get_block_hash.return_value, + ) + subtensor.substrate.runtime_call.assert_called_once_with( + "NeuronInfoRuntimeApi", + "get_neuron", + [fake_netuid, subtensor.substrate.query.return_value.value], + subtensor.substrate.get_block_hash.return_value, + ) + assert result == mock_neuron_from_dict.return_value + + +def test_neuron_for_uid_none(subtensor, mocker): + """Test neuron_for_uid successful call.""" + # Prep + fake_uid = None + fake_netuid = 2 + fake_block = 123 + mocked_neuron_info = mocker.patch.object( + subtensor_module.NeuronInfo, "get_null_neuron" + ) + + # Call + result = subtensor.neuron_for_uid( + uid=fake_uid, netuid=fake_netuid, block=fake_block + ) + + # Asserts + mocked_neuron_info.assert_called_once() + assert result == mocked_neuron_info.return_value + + +def test_neuron_for_uid_response_none(subtensor, mocker): + """Test neuron_for_uid successful call.""" + # Prep + fake_uid = 1 + fake_netuid = 2 + fake_block = 123 + mocked_neuron_info = mocker.patch.object( + subtensor_module.NeuronInfo, "get_null_neuron" + ) + + subtensor.substrate.runtime_call.return_value.value = None + + # Call + result = subtensor.neuron_for_uid( + uid=fake_uid, netuid=fake_netuid, block=fake_block + ) + + # Asserts + subtensor.substrate.get_block_hash.assert_called_once_with(fake_block) + subtensor.substrate.runtime_call.assert_called_once_with( + "NeuronInfoRuntimeApi", + "get_neuron", + [fake_netuid, fake_uid], + subtensor.substrate.get_block_hash.return_value, + ) + + mocked_neuron_info.assert_called_once() + assert result == mocked_neuron_info.return_value + + +def test_neuron_for_uid_success(subtensor, mocker): + """Test neuron_for_uid successful call.""" + # Prep + fake_uid = 1 + fake_netuid = 2 + fake_block = 123 + mocked_neuron_from_dict = mocker.patch.object( + subtensor_module.NeuronInfo, "from_dict" + ) + + # Call + result = subtensor.neuron_for_uid( + uid=fake_uid, netuid=fake_netuid, block=fake_block + ) + + # Asserts + subtensor.substrate.get_block_hash.assert_called_once_with(fake_block) + subtensor.substrate.runtime_call.assert_called_once_with( + "NeuronInfoRuntimeApi", + "get_neuron", + [fake_netuid, fake_uid], + subtensor.substrate.get_block_hash.return_value, + ) + + assert result == mocked_neuron_from_dict.return_value + + +def test_immunity_period(subtensor, mocker): + """Successful immunity_period call.""" + # Preps + fake_netuid = 1 + fake_block = 123 + fake_result = 101 + + mocked_get_hyperparameter = mocker.patch.object( + subtensor, + "get_hyperparameter", + return_value=fake_result, + ) + + # Call + result = subtensor.immunity_period(netuid=fake_netuid, block=fake_block) + + # Assertions + mocked_get_hyperparameter.assert_called_once_with( + param_name="ImmunityPeriod", + netuid=fake_netuid, + block=fake_block, + ) + assert result == mocked_get_hyperparameter.return_value + + +def test_get_uid_for_hotkey_on_subnet(subtensor, mocker): + """Successful get_uid_for_hotkey_on_subnet call.""" + # Prep + fake_hotkey_ss58 = "fake_hotkey_ss58" + fake_netuid = 1 + fake_block = 123 + mocked_query_subtensor = mocker.MagicMock() + subtensor.substrate.query = mocked_query_subtensor + + # Call + result = subtensor.get_uid_for_hotkey_on_subnet( + hotkey_ss58=fake_hotkey_ss58, netuid=fake_netuid, block=fake_block + ) + + # Assertions + mocked_query_subtensor.assert_called_once_with( + module="SubtensorModule", + storage_function="Uids", + params=[fake_netuid, 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 == mocked_query_subtensor.return_value.value + + +def test_tempo(subtensor, mocker): + """Successful tempo call.""" + # Preps + fake_netuid = 1 + fake_block = 123 + fake_result = 101 + + mocked_get_hyperparameter = mocker.patch.object( + subtensor, + "get_hyperparameter", + return_value=fake_result, + ) + + # Call + result = subtensor.tempo(netuid=fake_netuid, block=fake_block) + + # Assertions + mocked_get_hyperparameter.assert_called_once_with( + param_name="Tempo", + netuid=fake_netuid, + block=fake_block, + ) + assert result == mocked_get_hyperparameter.return_value + + +def test_get_commitment(subtensor, mocker): + """Successful get_commitment call.""" + # Preps + fake_netuid = 1 + fake_uid = 2 + fake_block = 3 + fake_hotkey = "hotkey" + expected_result = ( + "{'peer_id': '12D3KooWFWnHBmUFxvfL6PfZ5eGHdhgsEqNnsxuN1HE9EtfW8THi', " + "'model_huggingface_id': 'kmfoda/gpt2-1b-miner-3'}" + ) + + mocked_metagraph = mocker.MagicMock() + subtensor.metagraph = mocked_metagraph + mocked_metagraph.return_value.hotkeys = {fake_uid: fake_hotkey} + + mocked_get_metadata = mocker.patch.object(subtensor_module, "get_metadata") + mocked_get_metadata.return_value = { + "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, + ), + ) + }, + ), + ) + }, + } + + # Call + result = subtensor.get_commitment( + netuid=fake_netuid, uid=fake_uid, block=fake_block + ) + + # Assertions + mocked_metagraph.assert_called_once_with(fake_netuid) + assert result == expected_result + + +def test_get_last_commitment_bonds_reset_block(subtensor, mocker): + """Successful get_last_commitment_bonds_reset_block call.""" + # Preps + fake_netuid = 1 + fake_uid = 2 + fake_hotkey = "hotkey" + expected_result = 3 + + mocked_get_last_bonds_reset = mocker.patch.object( + subtensor_module, "get_last_bonds_reset" + ) + mocked_get_last_bonds_reset.return_value = expected_result + + mocked_metagraph = mocker.MagicMock() + subtensor.metagraph = mocked_metagraph + mocked_metagraph.return_value.hotkeys = {fake_uid: fake_hotkey} + + # Call + result = subtensor.get_last_commitment_bonds_reset_block( + netuid=fake_netuid, uid=fake_uid + ) + + # Assertions + mocked_get_last_bonds_reset.assert_called_once() + assert result == expected_result + + +def test_min_allowed_weights(subtensor, mocker): + """Successful min_allowed_weights call.""" + fake_netuid = 1 + fake_block = 123 + return_value = 10 + + mocked_get_hyperparameter = mocker.patch.object( + subtensor, + "get_hyperparameter", + return_value=return_value, + ) + + # Call + result = subtensor.min_allowed_weights(netuid=fake_netuid, block=fake_block) + + # Assertion + mocked_get_hyperparameter.assert_called_once_with( + param_name="MinAllowedWeights", block=fake_block, netuid=fake_netuid + ) + assert result == return_value + + +def test_max_weight_limit(subtensor, mocker): + """Successful max_weight_limit call.""" + fake_netuid = 1 + fake_block = 123 + return_value = 100 + + mocked_get_hyperparameter = mocker.patch.object( + subtensor, + "get_hyperparameter", + return_value=return_value, + ) + + mocked_u16_normalized_float = mocker.patch.object( + subtensor_module, + "u16_normalized_float", + ) + + # Call + result = subtensor.max_weight_limit(netuid=fake_netuid, block=fake_block) + + # Assertion + mocked_get_hyperparameter.assert_called_once_with( + param_name="MaxWeightsLimit", block=fake_block, netuid=fake_netuid + ) + assert result == mocked_u16_normalized_float.return_value + + +def test_get_transfer_fee(subtensor, fake_wallet, mocker): + """Successful get_transfer_fee call.""" + # Preps + fake_dest = "SS58ADDRESS" + value = Balance(1) + + fake_payment_info = {"partial_fee": int(2e10)} + subtensor.substrate.get_payment_info.return_value = fake_payment_info + + # Call + result = subtensor.get_transfer_fee(wallet=fake_wallet, dest=fake_dest, value=value) + + # Asserts + subtensor.substrate.compose_call.assert_called_once_with( + call_module="Balances", + call_function="transfer_keep_alive", + call_params={"dest": fake_dest, "value": value.rao}, + ) + + subtensor.substrate.get_payment_info.assert_called_once_with( + call=subtensor.substrate.compose_call.return_value, + keypair=fake_wallet.coldkeypub, + ) + + assert result == 2e10 + + +def test_get_existential_deposit(subtensor, mocker): + """Successful get_existential_deposit call.""" + # Prep + block = 123 + + mocked_query_constant = mocker.MagicMock() + value = 10 + mocked_query_constant.return_value.value = value + subtensor.substrate.get_constant = mocked_query_constant + + # Call + result = subtensor.get_existential_deposit(block=block) + + # Assertions + mocked_query_constant.assert_called_once_with( + module_name="Balances", + constant_name="ExistentialDeposit", + block_hash=subtensor.substrate.get_block_hash.return_value, + ) + subtensor.substrate.get_block_hash.assert_called_once_with(block) + + assert isinstance(result, Balance) + assert result == Balance.from_rao(value) + + +def test_commit_weights(subtensor, fake_wallet, mocker): + """Successful commit_weights call.""" + # Preps + netuid = 1 + salt = [1, 3] + uids = [2, 4] + weights = [0.4, 0.6] + wait_for_inclusion = False + wait_for_finalization = False + max_retries = 5 + + expected_result = (True, None) + mocked_generate_weight_hash = mocker.patch.object( + subtensor_module, "generate_weight_hash", return_value=expected_result + ) + mocked_commit_weights_extrinsic = mocker.patch.object( + subtensor_module, "commit_weights_extrinsic", return_value=expected_result + ) + + # Call + result = subtensor.commit_weights( + wallet=fake_wallet, + netuid=netuid, + salt=salt, + uids=uids, + weights=weights, + version_key=settings.version_as_int, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + max_retries=max_retries, + ) + + # Asserts + mocked_generate_weight_hash.assert_called_once_with( + address=fake_wallet.hotkey.ss58_address, + netuid=netuid, + uids=list(uids), + values=list(weights), + salt=list(salt), + version_key=settings.version_as_int, + ) + + mocked_commit_weights_extrinsic.assert_called_once_with( + subtensor=subtensor, + wallet=fake_wallet, + netuid=netuid, + commit_hash=mocked_generate_weight_hash.return_value, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + period=16, + raise_error=True, + ) + assert result == expected_result + + +def test_reveal_weights(subtensor, fake_wallet, mocker): + """Successful test_reveal_weights call.""" + # Preps + netuid = 1 + uids = [1, 2, 3, 4] + weights = [0.1, 0.2, 0.3, 0.4] + salt = [4, 2, 2, 1] + expected_result = (True, None) + mocked_extrinsic = mocker.patch.object( + subtensor_module, "reveal_weights_extrinsic", return_value=expected_result + ) + + # Call + result = subtensor.reveal_weights( + wallet=fake_wallet, + netuid=netuid, + uids=uids, + weights=weights, + salt=salt, + wait_for_inclusion=False, + wait_for_finalization=False, + ) + + # Assertions + assert result == (True, None) + mocked_extrinsic.assert_called_once_with( + subtensor=subtensor, + wallet=fake_wallet, + netuid=netuid, + uids=uids, + version_key=version_as_int, + weights=weights, + salt=salt, + wait_for_inclusion=False, + wait_for_finalization=False, + period=16, + raise_error=True, + ) + + +def test_reveal_weights_false(subtensor, fake_wallet, mocker): + """Failed test_reveal_weights call.""" + # Preps + netuid = 1 + uids = [1, 2, 3, 4] + weights = [0.1, 0.2, 0.3, 0.4] + salt = [4, 2, 2, 1] + + expected_result = ( + False, + "No attempt made. Perhaps it is too soon to reveal weights!", + ) + mocked_extrinsic = mocker.patch.object(subtensor_module, "reveal_weights_extrinsic") + + # Call + result = subtensor.reveal_weights( + wallet=fake_wallet, + netuid=netuid, + uids=uids, + weights=weights, + salt=salt, + wait_for_inclusion=False, + wait_for_finalization=False, + ) + + # Assertion + assert result == expected_result + assert mocked_extrinsic.call_count == 5 + + +def test_get_subnet_burn_cost_success(subtensor, mocker): + """Tests get_subnet_burn_cost method with successfully result.""" + # Preps + mocked_query_runtime_api = mocker.patch.object( + subtensor, "query_runtime_api", return_value=1000 + ) + fake_block = 123 + + # Call + result = subtensor.get_subnet_burn_cost(fake_block) + + # Asserts + mocked_query_runtime_api.assert_called_once_with( + runtime_api="SubnetRegistrationRuntimeApi", + method="get_network_registration_cost", + params=[], + block=fake_block, + ) + + assert result == mocked_query_runtime_api.return_value + + +def test_get_subnet_burn_cost_none(subtensor, mocker): + """Tests get_subnet_burn_cost method with None result.""" + # Preps + mocked_query_runtime_api = mocker.patch.object( + subtensor, "query_runtime_api", return_value=None + ) + fake_block = 123 + + # Call + result = subtensor.get_subnet_burn_cost(fake_block) + + # Asserts + mocked_query_runtime_api.assert_called_once_with( + runtime_api="SubnetRegistrationRuntimeApi", + method="get_network_registration_cost", + params=[], + block=fake_block, + ) + + assert result is None + + +def test_difficulty_success(subtensor, mocker): + """Tests difficulty method with successfully result.""" + # Preps + mocked_get_hyperparameter = mocker.patch.object(subtensor, "get_hyperparameter") + 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 == 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 + mocked_get_hyperparameter = mocker.patch.object( + subtensor, "get_hyperparameter", return_value=0.1 + ) + fake_netuid = 1 + fake_block = 2 + mocked_balance = mocker.patch("bittensor.core.subtensor.Balance") + + # Call + result = subtensor.recycle(fake_netuid, fake_block) + + # Asserts + mocked_get_hyperparameter.assert_called_once_with( + param_name="Burn", + netuid=fake_netuid, + block=fake_block, + ) + + mocked_balance.from_rao.assert_called_once_with( + int(mocked_get_hyperparameter.return_value) + ) + assert result == mocked_balance.from_rao.return_value + + +def test_recycle_none(subtensor, mocker): + """Tests recycle 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.recycle(fake_netuid, fake_block) + + # Asserts + mocked_get_hyperparameter.assert_called_once_with( + param_name="Burn", + netuid=fake_netuid, + block=fake_block, + ) + + assert result is None + + +# `get_all_subnets_info` tests +def test_get_all_subnets_info_success(mocker, subtensor): + """Test get_all_subnets_info returns correct data when subnet information is found.""" + # Prep + block = 123 + + mocker.patch.object(subtensor, "query_runtime_api") + mocker.patch.object( + subtensor_module.SubnetInfo, + "list_from_dicts", + ) + + # Call + subtensor.get_all_subnets_info(block) + + # Asserts + subtensor.query_runtime_api.assert_called_once_with( + runtime_api="SubnetInfoRuntimeApi", + method="get_subnets_info_v2", + params=[], + block=block, + ) + subtensor_module.SubnetInfo.list_from_dicts.assert_called_once_with( + subtensor.query_runtime_api.return_value, + ) + + +@pytest.mark.parametrize("result_", [[], None]) +def test_get_all_subnets_info_no_data(mocker, subtensor, result_): + """Test get_all_subnets_info returns empty list when no subnet information is found.""" + # Prep + block = 123 + mocker.patch.object( + subtensor.substrate, "get_block_hash", return_value="mock_block_hash" + ) + mocker.patch.object(subtensor_module.SubnetInfo, "list_from_dicts") + + mocker.patch.object(subtensor, "query_runtime_api", return_value=result_) + + # Call + result = subtensor.get_all_subnets_info(block) + + # Asserts + assert result == [] + subtensor.query_runtime_api.assert_called_once_with( + runtime_api="SubnetInfoRuntimeApi", + method="get_subnets_info_v2", + params=[], + block=block, + ) + subtensor_module.SubnetInfo.list_from_dicts.assert_not_called() + + +def test_get_delegate_take_success(subtensor, mocker): + """Verify `get_delegate_take` method successful path.""" + # Preps + fake_hotkey_ss58 = "FAKE_SS58" + fake_block = 123 + + mocker.patch.object(subtensor_module, "u16_normalized_float") + subtensor.query_subtensor = mocker.Mock(return_value=mocker.Mock(value="value")) + + # Call + result = subtensor.get_delegate_take(hotkey_ss58=fake_hotkey_ss58, block=fake_block) + + # Asserts + subtensor.query_subtensor.assert_called_once_with( + name="Delegates", + block=fake_block, + params=[fake_hotkey_ss58], + ) + subtensor_module.u16_normalized_float.assert_called_once_with( + subtensor.query_subtensor.return_value.value + ) + assert result == subtensor_module.u16_normalized_float.return_value + + +def test_networks_during_connection(mock_substrate, mocker): + """Test networks during_connection.""" + # Preps + mocker.patch("websockets.sync.client.connect") + # Call + for network in list(settings.NETWORK_MAP.keys()) + ["undefined"]: + sub = Subtensor(network) + + # Assertions + sub.network = network + sub.chain_endpoint = settings.NETWORK_MAP.get(network) + + +def test_get_stake_for_coldkey_and_hotkey(subtensor, mocker): + netuids = [1, 2, 3] + stake_info_dict = { + "netuid": 1, + "hotkey": b"\x16:\xech\r\xde,g\x03R1\xb9\x88q\xe79\xb8\x88\x93\xae\xd2)?*\rp\xb2\xe62\xads\x1c", + "coldkey": b"\x16:\xech\r\xde,g\x03R1\xb9\x88q\xe79\xb8\x88\x93\xae\xd2)?*\rp\xb2\xe62\xads\x1c", + "stake": 1, + "locked": False, + "emission": 1, + "drain": 1, + "is_registered": True, + } + query_result = stake_info_dict + expected_result = { + netuid: StakeInfo.from_dict(stake_info_dict) for netuid in netuids + } + + query_fetcher = mocker.Mock(return_value=query_result) + + mocked_query_runtime_api = mocker.patch.object( + subtensor, "query_runtime_api", side_effect=query_fetcher + ) + mocked_get_subnets = mocker.patch.object( + subtensor, "get_subnets", return_value=netuids + ) + + result = subtensor.get_stake_for_coldkey_and_hotkey( + hotkey_ss58="hotkey", coldkey_ss58="coldkey", block=None, netuids=None + ) + + assert result == expected_result + + # validate that mocked functions were called with the right arguments + mocked_query_runtime_api.assert_has_calls( + [ + mock.call( + "StakeInfoRuntimeApi", + "get_stake_info_for_hotkey_coldkey_netuid", + params=["hotkey", "coldkey", netuid], + block=None, + ) + for netuid in netuids + ] + ) + mocked_get_subnets.assert_called_once_with(block=None) + + +def test_does_hotkey_exist_true(mocker, subtensor): + """Test when the hotkey exists.""" + # Mock data + fake_hotkey_ss58 = "fake_hotkey" + fake_owner = "valid_owner" + fake_block = 123 + + # Mocks + mock_query_subtensor = mocker.patch.object( + subtensor.substrate, + "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) + + # 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 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): + """Test when query_subtensor returns the special invalid owner identifier.""" + # Mock data + fake_hotkey_ss58 = "fake_hotkey" + fake_owner = "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM" + fake_block = 123 + + # 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, + ) + + # 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_latest_block(mocker, subtensor): + """Test when no block is provided (latest block).""" + # Mock data + fake_hotkey_ss58 = "fake_hotkey" + fake_owner = "valid_owner" + + # Mocks + mock_query_subtensor = mocker.patch.object( + subtensor.substrate, + "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) + + # Assertions + mock_query_subtensor.assert_called_once_with( + module="SubtensorModule", + storage_function="Owner", + params=[fake_hotkey_ss58], + block_hash=None, + ) + assert result is True + + +def test_get_hotkey_owner_success(mocker, subtensor): + """Test when hotkey exists and owner is found.""" + # Mock data + fake_hotkey_ss58 = "fake_hotkey" + fake_coldkey_ss58 = "fake_coldkey" + fake_block = 123 + + # Mocks + mock_query_subtensor = mocker.patch.object( + subtensor.substrate, "query", return_value=fake_coldkey_ss58 + ) + 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_called_once_with(fake_hotkey_ss58, block=fake_block) + subtensor.substrate.get_block_hash.assert_called_once_with(fake_block) + 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 + fake_hotkey_ss58 = "fake_hotkey" + fake_block = 123 + + # Mocks + mock_query_subtensor = mocker.patch.object( + subtensor.substrate, + "query", + return_value=mocker.Mock(value=[fake_hotkey_ss58]), + ) + 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) + + # 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_called_once_with(fake_hotkey_ss58, block=fake_block) + subtensor.substrate.get_block_hash.assert_called_once_with(fake_block) + assert result is None + + +def test_get_hotkey_owner_latest_block(mocker, subtensor): + """Test when no block is provided (latest block).""" + # Mock data + fake_hotkey_ss58 = "fake_hotkey" + fake_coldkey_ss58 = "fake_coldkey" + + # Mocks + mock_query_subtensor = mocker.patch.object( + subtensor.substrate, "query", return_value=fake_coldkey_ss58 + ) + mock_does_hotkey_exist = mocker.patch.object( + subtensor, "does_hotkey_exist", return_value=True + ) + + # Call + result = subtensor.get_hotkey_owner(fake_hotkey_ss58) + + # Assertions + mock_query_subtensor.assert_called_once_with( + module="SubtensorModule", + storage_function="Owner", + params=[fake_hotkey_ss58], + block_hash=None, + ) + mock_does_hotkey_exist.assert_called_once_with(fake_hotkey_ss58, block=None) + assert result == fake_coldkey_ss58 + + +def test_get_minimum_required_stake_success(mocker, subtensor): + """Test successful call to get_minimum_required_stake.""" + # Mock data + fake_min_stake = "1000000000" # Example value in rao + + # Mocking + mock_query = mocker.patch.object( + subtensor.substrate, + "query", + return_value=mocker.Mock(value=fake_min_stake), + ) + mock_balance_from_rao = mocker.patch("bittensor.utils.balance.Balance.from_rao") + + # Call + result = subtensor.get_minimum_required_stake() + + # Assertions + mock_query.assert_called_once_with( + module="SubtensorModule", storage_function="NominatorMinRequiredStake" + ) + mock_balance_from_rao.assert_called_once_with(fake_min_stake) + assert result == mock_balance_from_rao.return_value + + +def test_get_minimum_required_stake_query_failure(mocker, subtensor): + """Test query failure in get_minimum_required_stake.""" + # Mocking + mock_query = mocker.patch.object( + subtensor.substrate, + "query", + side_effect=Exception("Query failed"), + ) + + # Call and Assertions + with pytest.raises(Exception, match="Query failed"): + subtensor.get_minimum_required_stake() + mock_query.assert_called_once_with( + module="SubtensorModule", storage_function="NominatorMinRequiredStake" + ) + + +def test_get_minimum_required_stake_invalid_result(mocker, subtensor): + """Test when the result cannot be decoded.""" + # Mock data + fake_invalid_stake = None # Simulate a failure in decoding + + # Mocking + mock_query = mocker.patch.object( + subtensor.substrate, + "query", + return_value=mocker.Mock(value=fake_invalid_stake), + ) + mock_balance_from_rao = mocker.patch("bittensor.utils.balance.Balance.from_rao") + + # Call + result = subtensor.get_minimum_required_stake() + + # Assertions + mock_query.assert_called_once_with( + module="SubtensorModule", storage_function="NominatorMinRequiredStake" + ) + mock_balance_from_rao.assert_called_once_with(fake_invalid_stake) + assert result == mock_balance_from_rao.return_value + + +def test_tx_rate_limit_success(mocker, subtensor): + """Test when tx_rate_limit is successfully retrieved.""" + # Mock data + fake_rate_limit = 100 + fake_block = 123 + + # Mocks + mock_query_subtensor = mocker.patch.object( + subtensor.substrate, + "query", + return_value=mocker.Mock(value=fake_rate_limit), + ) + + # Call + result = subtensor.tx_rate_limit(block=fake_block) + + # Assertions + mock_query_subtensor.assert_called_once_with( + module="SubtensorModule", + storage_function="TxRateLimit", + params=None, + block_hash=subtensor.substrate.get_block_hash.return_value, + ) + subtensor.substrate.get_block_hash.assert_called_once_with(fake_block) + assert result == fake_rate_limit + + +def test_tx_rate_limit_no_value(mocker, subtensor): + """Test when query_subtensor returns None.""" + # Mock data + fake_block = 123 + + # Mocks + mock_query_subtensor = mocker.patch.object( + subtensor.substrate, + "query", + return_value=None, + ) + + # Call + result = subtensor.tx_rate_limit(block=fake_block) + + # Assertions + mock_query_subtensor.assert_called_once_with( + module="SubtensorModule", + storage_function="TxRateLimit", + params=None, + block_hash=subtensor.substrate.get_block_hash.return_value, + ) + subtensor.substrate.get_block_hash.assert_called_once_with(fake_block) + assert result is None + + +def test_get_delegates_success(mocker, subtensor): + """Test when delegates are successfully retrieved.""" + # Mock data + fake_block = 123 + + # Mocks + mock_query_runtime_api = mocker.patch.object( + subtensor, + "query_runtime_api", + ) + mock_list_from_dicts = mocker.patch.object( + subtensor_module.DelegateInfo, + "list_from_dicts", + ) + + # Call + result = subtensor.get_delegates(block=fake_block) + + # Assertions + mock_query_runtime_api.assert_called_once_with( + runtime_api="DelegateInfoRuntimeApi", + method="get_delegates", + params=[], + block=123, + ) + mock_list_from_dicts.assert_called_once_with(mock_query_runtime_api.return_value) + + assert result == mock_list_from_dicts.return_value + + +def test_get_delegates_no_result(mocker, subtensor): + """Test when rpc_request returns no result.""" + # Mock data + fake_block = 123 + + # Mocks + mock_query_runtime_api = mocker.patch.object( + subtensor, + "query_runtime_api", + return_value=None, + ) + mock_list_from_dicts = mocker.patch.object( + subtensor_module.DelegateInfo, + "list_from_dicts", + ) + + # Call + result = subtensor.get_delegates(block=fake_block) + + # Assertions + mock_query_runtime_api.assert_called_once_with( + runtime_api="DelegateInfoRuntimeApi", + method="get_delegates", + params=[], + block=123, + ) + mock_list_from_dicts.assert_not_called() + + assert result == [] + + +def test_is_hotkey_delegate_true(mocker, subtensor): + """Test when hotkey is a delegate.""" + # Mock data + fake_hotkey_ss58 = "hotkey_1" + fake_block = 123 + fake_delegates = [ + mocker.Mock(hotkey_ss58="hotkey_1"), + mocker.Mock(hotkey_ss58="hotkey_2"), + ] + + # Mocks + mock_get_delegates = mocker.patch.object( + subtensor, "get_delegates", return_value=fake_delegates + ) + + # Call + result = subtensor.is_hotkey_delegate(fake_hotkey_ss58, block=fake_block) + + # Assertions + mock_get_delegates.assert_called_once_with(fake_block) + assert result is True + + +def test_is_hotkey_delegate_false(mocker, subtensor): + """Test when hotkey is not a delegate.""" + # Mock data + fake_hotkey_ss58 = "hotkey_3" + fake_block = 123 + fake_delegates = [ + mocker.Mock(hotkey_ss58="hotkey_1"), + mocker.Mock(hotkey_ss58="hotkey_2"), + ] + + # Mocks + mock_get_delegates = mocker.patch.object( + subtensor, "get_delegates", return_value=fake_delegates + ) + + # Call + result = subtensor.is_hotkey_delegate(fake_hotkey_ss58, block=fake_block) + + # Assertions + mock_get_delegates.assert_called_once_with(fake_block) + assert result is False + + +def test_is_hotkey_delegate_empty_list(mocker, subtensor): + """Test when delegate list is empty.""" + # Mock data + fake_hotkey_ss58 = "hotkey_1" + fake_block = 123 + + # Mocks + mock_get_delegates = mocker.patch.object( + subtensor, "get_delegates", return_value=[] + ) + + # Call + result = subtensor.is_hotkey_delegate(fake_hotkey_ss58, block=fake_block) + + # Assertions + mock_get_delegates.assert_called_once_with(fake_block) + assert result is False + + +def test_add_stake_success(mocker, fake_wallet, subtensor): + """Test add_stake returns True on successful staking.""" + # Prep + fake_hotkey_ss58 = "fake_hotkey" + fake_amount = Balance.from_tao(10.0) + fake_netuid = 14 + + mock_add_stake_extrinsic = mocker.patch.object( + subtensor_module, "add_stake_extrinsic" + ) + + # Call + result = subtensor.add_stake( + wallet=fake_wallet, + netuid=fake_netuid, + hotkey_ss58=fake_hotkey_ss58, + amount=fake_amount, + wait_for_inclusion=True, + wait_for_finalization=False, + safe_staking=False, + allow_partial_stake=False, + rate_tolerance=0.005, + ) + + # Assertions + mock_add_stake_extrinsic.assert_called_once_with( + subtensor=subtensor, + wallet=fake_wallet, + hotkey_ss58=fake_hotkey_ss58, + netuid=14, + amount=fake_amount.rao, + wait_for_inclusion=True, + wait_for_finalization=False, + safe_staking=False, + allow_partial_stake=False, + rate_tolerance=0.005, + period=None, + raise_error=False, + ) + assert result == mock_add_stake_extrinsic.return_value + + +def test_add_stake_with_safe_staking(mocker, fake_wallet, subtensor): + """Test add_stake with safe staking parameters enabled.""" + # Prep + fake_netuid = 14 + fake_hotkey_ss58 = "fake_hotkey" + fake_amount = Balance.from_tao(10.0) + fake_rate_tolerance = 0.01 # 1% threshold + + mock_add_stake_extrinsic = mocker.patch.object( + subtensor_module, "add_stake_extrinsic" + ) + + # Call + result = subtensor.add_stake( + wallet=fake_wallet, + netuid=fake_netuid, + hotkey_ss58=fake_hotkey_ss58, + amount=fake_amount, + wait_for_inclusion=True, + wait_for_finalization=False, + safe_staking=True, + allow_partial_stake=False, + rate_tolerance=fake_rate_tolerance, + ) + + # Assertions + mock_add_stake_extrinsic.assert_called_once_with( + subtensor=subtensor, + wallet=fake_wallet, + hotkey_ss58=fake_hotkey_ss58, + netuid=14, + amount=fake_amount.rao, + wait_for_inclusion=True, + wait_for_finalization=False, + safe_staking=True, + allow_partial_stake=False, + rate_tolerance=fake_rate_tolerance, + period=None, + raise_error=False, + ) + assert result == mock_add_stake_extrinsic.return_value + + +def test_add_stake_multiple_success(mocker, fake_wallet, subtensor): + """Test add_stake_multiple successfully stakes for all hotkeys.""" + # Prep + fake_hotkey_ss58 = ["fake_hotkey"] + fake_amount = [10.0] + + mock_add_stake_multiple_extrinsic = mocker.patch.object( + subtensor_module, "add_stake_multiple_extrinsic" + ) + + # Call + result = subtensor.add_stake_multiple( + wallet=fake_wallet, + hotkey_ss58s=fake_hotkey_ss58, + netuids=[1], + amounts=fake_amount, + wait_for_inclusion=True, + wait_for_finalization=False, + ) + + # Assertions + mock_add_stake_multiple_extrinsic.assert_called_once_with( + subtensor=subtensor, + wallet=fake_wallet, + hotkey_ss58s=fake_hotkey_ss58, + netuids=[1], + amounts=fake_amount, + wait_for_inclusion=True, + wait_for_finalization=False, + period=None, + raise_error=False, + ) + assert result == mock_add_stake_multiple_extrinsic.return_value + + +def test_unstake_success(mocker, subtensor, fake_wallet): + """Test unstake operation is successful.""" + # Preps + fake_hotkey_ss58 = "hotkey_1" + fake_netuid = 1 + fake_amount = 10.0 + + mock_unstake_extrinsic = mocker.patch.object(subtensor_module, "unstake_extrinsic") + + # Call + result = subtensor.unstake( + wallet=fake_wallet, + netuid=fake_netuid, + hotkey_ss58=fake_hotkey_ss58, + amount=fake_amount, + wait_for_inclusion=True, + wait_for_finalization=False, + safe_unstaking=False, + allow_partial_stake=False, + rate_tolerance=0.005, + ) + + # Assertions + mock_unstake_extrinsic.assert_called_once_with( + subtensor=subtensor, + wallet=fake_wallet, + netuid=fake_netuid, + hotkey_ss58=fake_hotkey_ss58, + amount=Balance.from_rao(fake_amount), + safe_unstaking=False, + allow_partial_stake=False, + rate_tolerance=0.005, + period=None, + wait_for_inclusion=True, + wait_for_finalization=False, + raise_error=False, + ) + assert result == mock_unstake_extrinsic.return_value + + +def test_unstake_with_safe_unstaking(mocker, subtensor, fake_wallet): + """Test unstake with `safe_unstaking` parameters enabled.""" + fake_hotkey_ss58 = "hotkey_1" + fake_amount = 10.0 + fake_netuid = 14 + fake_rate_tolerance = 0.01 # 1% threshold + + mock_unstake_extrinsic = mocker.patch.object(subtensor_module, "unstake_extrinsic") + + # Call + result = subtensor.unstake( + wallet=fake_wallet, + netuid=fake_netuid, + hotkey_ss58=fake_hotkey_ss58, + amount=fake_amount, + wait_for_inclusion=True, + wait_for_finalization=False, + safe_unstaking=True, + allow_partial_stake=True, + rate_tolerance=fake_rate_tolerance, + ) + + # Assertions + mock_unstake_extrinsic.assert_called_once_with( + subtensor=subtensor, + wallet=fake_wallet, + netuid=fake_netuid, + hotkey_ss58=fake_hotkey_ss58, + amount=Balance.from_rao(fake_amount), + safe_unstaking=True, + allow_partial_stake=True, + rate_tolerance=fake_rate_tolerance, + period=None, + raise_error=False, + wait_for_inclusion=True, + wait_for_finalization=False, + ) + assert result == mock_unstake_extrinsic.return_value + + +def test_swap_stake_success(mocker, subtensor, fake_wallet): + """Test swap_stake operation is successful.""" + # Preps + fake_hotkey_ss58 = "hotkey_1" + fake_origin_netuid = 1 + fake_destination_netuid = 2 + fake_amount = 10.0 + + mock_swap_stake_extrinsic = mocker.patch.object( + subtensor_module, "swap_stake_extrinsic" + ) + + # Call + result = subtensor.swap_stake( + wallet=fake_wallet, + hotkey_ss58=fake_hotkey_ss58, + origin_netuid=fake_origin_netuid, + destination_netuid=fake_destination_netuid, + amount=fake_amount, + wait_for_inclusion=True, + wait_for_finalization=False, + safe_swapping=False, + allow_partial_stake=False, + rate_tolerance=0.005, + ) + + # Assertions + mock_swap_stake_extrinsic.assert_called_once_with( + subtensor=subtensor, + wallet=fake_wallet, + hotkey_ss58=fake_hotkey_ss58, + origin_netuid=fake_origin_netuid, + destination_netuid=fake_destination_netuid, + amount=Balance.from_rao(fake_amount), + wait_for_inclusion=True, + wait_for_finalization=False, + safe_swapping=False, + allow_partial_stake=False, + rate_tolerance=0.005, + period=None, + raise_error=False, + ) + assert result == mock_swap_stake_extrinsic.return_value + + +def test_swap_stake_with_safe_staking(mocker, subtensor, fake_wallet): + """Test swap_stake with safe staking parameters enabled.""" + # Preps + fake_hotkey_ss58 = "hotkey_1" + fake_origin_netuid = 1 + fake_destination_netuid = 2 + fake_amount = 10.0 + fake_rate_tolerance = 0.01 # 1% threshold + + mock_swap_stake_extrinsic = mocker.patch.object( + subtensor_module, "swap_stake_extrinsic" + ) + + # Call + result = subtensor.swap_stake( + wallet=fake_wallet, + hotkey_ss58=fake_hotkey_ss58, + origin_netuid=fake_origin_netuid, + destination_netuid=fake_destination_netuid, + amount=fake_amount, + wait_for_inclusion=True, + wait_for_finalization=False, + safe_swapping=True, + allow_partial_stake=True, + rate_tolerance=fake_rate_tolerance, + ) + + # Assertions + mock_swap_stake_extrinsic.assert_called_once_with( + subtensor=subtensor, + wallet=fake_wallet, + hotkey_ss58=fake_hotkey_ss58, + origin_netuid=fake_origin_netuid, + destination_netuid=fake_destination_netuid, + amount=Balance.from_rao(fake_amount), + wait_for_inclusion=True, + wait_for_finalization=False, + safe_swapping=True, + allow_partial_stake=True, + rate_tolerance=fake_rate_tolerance, + period=None, + raise_error=False, + ) + assert result == mock_swap_stake_extrinsic.return_value + + +def test_unstake_multiple_success(mocker, subtensor, fake_wallet): + """Test unstake_multiple succeeds for all hotkeys.""" + # Preps + fake_hotkeys = ["hotkey_1", "hotkey_2"] + fake_amounts = [10.0, 20.0] + + mock_unstake_multiple_extrinsic = mocker.patch( + "bittensor.core.subtensor.unstake_multiple_extrinsic", return_value=True + ) + + # Call + result = subtensor.unstake_multiple( + wallet=fake_wallet, + hotkey_ss58s=fake_hotkeys, + netuids=[1, 2], + amounts=fake_amounts, + wait_for_inclusion=True, + wait_for_finalization=False, + ) + + # Assertions + mock_unstake_multiple_extrinsic.assert_called_once_with( + subtensor=subtensor, + wallet=fake_wallet, + hotkey_ss58s=fake_hotkeys, + netuids=[1, 2], + amounts=fake_amounts, + wait_for_inclusion=True, + wait_for_finalization=False, + period=None, + unstake_all=False, + raise_error=False, + ) + assert result == mock_unstake_multiple_extrinsic.return_value + + +def test_set_weights_with_commit_reveal_enabled(subtensor, fake_wallet, mocker): + """Test set_weights with commit_reveal_enabled is True.""" + # Preps + fake_netuid = 1 + fake_uids = [1, 5] + fake_weights = [0.1, 0.9] + fake_wait_for_inclusion = True + fake_wait_for_finalization = False + + mocked_commit_reveal_enabled = mocker.patch.object( + subtensor, "commit_reveal_enabled", return_value=True + ) + mocked_commit_reveal_v3_extrinsic = mocker.patch.object( + subtensor_module, "commit_reveal_extrinsic" + ) + mocked_commit_reveal_v3_extrinsic.return_value = ( + True, + "Weights committed successfully", + ) + mocker.patch.object(subtensor, "blocks_since_last_update", return_value=181) + mocker.patch.object(subtensor, "weights_rate_limit", return_value=180) + + # Call + result = subtensor.set_weights( + wallet=fake_wallet, + netuid=fake_netuid, + uids=fake_uids, + weights=fake_weights, + wait_for_inclusion=fake_wait_for_inclusion, + wait_for_finalization=fake_wait_for_finalization, + ) + + # Asserts + mocked_commit_reveal_enabled.assert_called_once_with(netuid=fake_netuid) + mocked_commit_reveal_v3_extrinsic.assert_called_once_with( + subtensor=subtensor, + wallet=fake_wallet, + netuid=fake_netuid, + uids=fake_uids, + weights=fake_weights, + commit_reveal_version=4, + version_key=subtensor_module.version_as_int, + wait_for_inclusion=fake_wait_for_inclusion, + wait_for_finalization=fake_wait_for_finalization, + block_time=12.0, + period=8, + raise_error=True, + ) + assert result == mocked_commit_reveal_v3_extrinsic.return_value + + +def test_connection_limit(mocker): + """Test connection limit is not exceeded.""" + # Technically speaking, this test should exist in integration tests. But to reduce server costs we will leave this + # test here. + + # Preps + mocker.patch.object( + sync_substrate, + "connect", + side_effect=websockets.InvalidStatus( + response=mocker.Mock( + response=mocker.Mock( + status_code=429, message="test connection limit error" + ) + ) + ), + ) + + # Call with assertions + with pytest.raises(websockets.InvalidStatus): + for i in range(2): + Subtensor("test") + + +def test_set_subnet_identity(mocker, subtensor, fake_wallet): + """Verify that subtensor method `set_subnet_identity` calls proper function with proper arguments.""" + # Preps + fake_netuid = 123 + fake_subnet_identity = mocker.MagicMock() + + mocked_extrinsic = mocker.patch.object( + subtensor_module, "set_subnet_identity_extrinsic" + ) + + # Call + result = subtensor.set_subnet_identity( + wallet=fake_wallet, netuid=fake_netuid, subnet_identity=fake_subnet_identity + ) + + # Asserts + mocked_extrinsic.assert_called_once_with( + subtensor=subtensor, + wallet=fake_wallet, + netuid=fake_netuid, + subnet_name=fake_subnet_identity.subnet_name, + github_repo=fake_subnet_identity.github_repo, + subnet_contact=fake_subnet_identity.subnet_contact, + subnet_url=fake_subnet_identity.subnet_url, + logo_url=fake_subnet_identity.logo_url, + discord=fake_subnet_identity.discord, + description=fake_subnet_identity.description, + additional=fake_subnet_identity.additional, + period=None, + raise_error=False, + wait_for_finalization=True, + wait_for_inclusion=True, + ) + assert result == mocked_extrinsic.return_value + + +def test_get_all_neuron_certificates(mocker, subtensor): + fake_netuid = 12 + mocked_query_map_subtensor = mocker.MagicMock() + mocker.patch.object(subtensor.substrate, "query_map", mocked_query_map_subtensor) + subtensor.get_all_neuron_certificates(fake_netuid) + mocked_query_map_subtensor.assert_called_once_with( + module="SubtensorModule", + storage_function="NeuronCertificates", + params=[fake_netuid], + block_hash=None, + ) + + +def test_get_timestamp(mocker, subtensor): + fake_block = 1000 + mocked_query = mocker.MagicMock(return_value=ScaleObj(1740586018 * 1000)) + mocker.patch.object(subtensor.substrate, "query", mocked_query) + expected_result = datetime.datetime( + 2025, 2, 26, 16, 6, 58, tzinfo=datetime.timezone.utc + ) + actual_result = subtensor.get_timestamp(block=fake_block) + assert expected_result == actual_result + + +def test_get_owned_hotkeys_happy_path(subtensor, mocker): + """Tests that the output of get_owned_hotkeys.""" + # Prep + fake_coldkey = "fake_hotkey" + fake_hotkey = "fake_hotkey" + fake_hotkeys = [ + [ + fake_hotkey, + ] + ] + mocked_subtensor = mocker.Mock(return_value=fake_hotkeys) + mocker.patch.object(subtensor.substrate, "query", new=mocked_subtensor) + + mocked_decode_account_id = mocker.Mock() + mocker.patch.object( + subtensor_module, "decode_account_id", new=mocked_decode_account_id + ) + + # Call + result = subtensor.get_owned_hotkeys(fake_coldkey) + + # Asserts + mocked_subtensor.assert_called_once_with( + module="SubtensorModule", + storage_function="OwnedHotkeys", + params=[fake_coldkey], + block_hash=None, + reuse_block_hash=False, + ) + assert result == [mocked_decode_account_id.return_value] + mocked_decode_account_id.assert_called_once_with(fake_hotkey) + + +def test_get_owned_hotkeys_return_empty(subtensor, mocker): + """Tests that the output of get_owned_hotkeys is empty.""" + # Prep + fake_coldkey = "fake_hotkey" + mocked_subtensor = mocker.Mock(return_value=[]) + mocker.patch.object(subtensor.substrate, "query", new=mocked_subtensor) + + # Call + result = subtensor.get_owned_hotkeys(fake_coldkey) + + # Asserts + mocked_subtensor.assert_called_once_with( + module="SubtensorModule", + storage_function="OwnedHotkeys", + params=[fake_coldkey], + block_hash=None, + reuse_block_hash=False, + ) + assert result == [] + + +def test_start_call(subtensor, mocker): + """Test start_call extrinsic calls properly.""" + # preps + wallet_name = mocker.Mock(spec=Wallet) + netuid = 123 + mocked_extrinsic = mocker.patch.object(subtensor_module, "start_call_extrinsic") + + # Call + result = subtensor.start_call(wallet_name, netuid) + + # Asserts + mocked_extrinsic.assert_called_once_with( + subtensor=subtensor, + wallet=wallet_name, + netuid=netuid, + wait_for_inclusion=True, + wait_for_finalization=False, + period=None, + raise_error=False, + ) + assert result == mocked_extrinsic.return_value + + +def test_get_metagraph_info_all_fields(subtensor, mocker): + """Test get_metagraph_info with all fields (default behavior).""" + # Preps + netuid = 1 + mock_value = {"mock": "data"} + + mock_runtime_call = mocker.patch.object( + subtensor.substrate, + "runtime_call", + return_value=mocker.Mock(value=mock_value), + ) + mock_from_dict = mocker.patch.object( + subtensor_module.MetagraphInfo, "from_dict", return_value="parsed_metagraph" + ) + + # Call + result = subtensor.get_metagraph_info( + netuid=netuid, field_indices=[f for f in range(73)] + ) + + # Asserts + assert result == "parsed_metagraph" + mock_runtime_call.assert_called_once_with( + "SubnetInfoRuntimeApi", + "get_selective_metagraph", + params=[netuid, SelectiveMetagraphIndex.all_indices()], + block_hash=subtensor.determine_block_hash(None), + ) + mock_from_dict.assert_called_once_with(mock_value) + + +def test_get_metagraph_info_specific_fields(subtensor, mocker): + """Test get_metagraph_info with specific fields.""" + # Preps + netuid = 1 + mock_value = {"mock": "data"} + fields = [SelectiveMetagraphIndex.Name, 5] + + mock_runtime_call = mocker.patch.object( + subtensor.substrate, + "runtime_call", + return_value=mocker.Mock(value=mock_value), + ) + mock_from_dict = mocker.patch.object( + subtensor_module.MetagraphInfo, "from_dict", return_value="parsed_metagraph" + ) + + # Call + result = subtensor.get_metagraph_info(netuid=netuid, field_indices=fields) + + # Asserts + assert result == "parsed_metagraph" + mock_runtime_call.assert_called_once_with( + "SubnetInfoRuntimeApi", + "get_selective_metagraph", + params=[ + netuid, + [0] + + [ + f.value if isinstance(f, SelectiveMetagraphIndex) else f for f in fields + ], + ], + block_hash=subtensor.determine_block_hash(None), + ) + mock_from_dict.assert_called_once_with(mock_value) + + +@pytest.mark.parametrize( + "wrong_fields", + [ + [ + "invalid", + ], + [SelectiveMetagraphIndex.Active, 1, "f"], + [1, 2, 3, "f"], + ], +) +def test_get_metagraph_info_invalid_field_indices(subtensor, wrong_fields): + """Test get_metagraph_info raises ValueError on invalid field_indices.""" + with pytest.raises( + ValueError, + match="`field_indices` must be a list of SelectiveMetagraphIndex enums or ints.", + ): + subtensor.get_metagraph_info(netuid=1, field_indices=wrong_fields) + + +def test_get_metagraph_info_subnet_not_exist(subtensor, mocker): + """Test get_metagraph_info returns None when subnet doesn't exist.""" + netuid = 1 + mocker.patch.object( + subtensor.substrate, + "runtime_call", + return_value=mocker.Mock(value=None), + ) + + mocked_logger = mocker.Mock() + mocker.patch("bittensor.core.subtensor.logging.error", new=mocked_logger) + + result = subtensor.get_metagraph_info(netuid=netuid) + + assert result is None + mocked_logger.assert_called_once_with(f"Subnet {netuid} does not exist.") + + +def test_blocks_since_last_step_with_value(subtensor, mocker): + """Test blocks_since_last_step returns correct value.""" + # preps + netuid = 1 + block = 123 + mocked_query_subtensor = mocker.MagicMock() + subtensor.query_subtensor = mocked_query_subtensor + + # call + result = subtensor.blocks_since_last_step(netuid=netuid, block=block) + + # asserts + mocked_query_subtensor.assert_called_once_with( + name="BlocksSinceLastStep", + block=block, + params=[netuid], + ) + + assert result == mocked_query_subtensor.return_value.value + + +def test_blocks_since_last_step_is_none(subtensor, mocker): + """Test blocks_since_last_step returns None correctly.""" + # preps + netuid = 1 + block = 123 + mocked_query_subtensor = mocker.MagicMock(return_value=None) + subtensor.query_subtensor = mocked_query_subtensor + + # call + result = subtensor.blocks_since_last_step(netuid=netuid, block=block) + + # asserts + mocked_query_subtensor.assert_called_once_with( + name="BlocksSinceLastStep", + block=block, + params=[netuid], + ) + + assert result is None + + +def test_get_subnet_owner_hotkey_has_return(subtensor, mocker): + """Test get_subnet_owner_hotkey returns correct value.""" + # preps + netuid = 14 + block = 123 + expected_owner_hotkey = "owner_hotkey" + mocked_query_subtensor = mocker.MagicMock(return_value=expected_owner_hotkey) + subtensor.query_subtensor = mocked_query_subtensor + + # call + result = subtensor.get_subnet_owner_hotkey(netuid=netuid, block=block) + + # asserts + mocked_query_subtensor.assert_called_once_with( + name="SubnetOwnerHotkey", + block=block, + params=[netuid], + ) + + assert result == expected_owner_hotkey + + +def test_get_subnet_owner_hotkey_is_none(subtensor, mocker): + """Test get_subnet_owner_hotkey returns None correctly.""" + # preps + netuid = 14 + block = 123 + mocked_query_subtensor = mocker.MagicMock(return_value=None) + subtensor.query_subtensor = mocked_query_subtensor + + # call + result = subtensor.get_subnet_owner_hotkey(netuid=netuid, block=block) + + # asserts + mocked_query_subtensor.assert_called_once_with( + name="SubnetOwnerHotkey", + block=block, + params=[netuid], + ) + + assert result is None + + +def test_get_subnet_validator_permits_has_values(subtensor, mocker): + """Test get_subnet_validator_permits returns correct value.""" + # preps + netuid = 14 + block = 123 + expected_validator_permits = [False, True, False] + mocked_query_subtensor = mocker.MagicMock(return_value=expected_validator_permits) + subtensor.query_subtensor = mocked_query_subtensor + + # call + result = subtensor.get_subnet_validator_permits(netuid=netuid, block=block) + + # asserts + mocked_query_subtensor.assert_called_once_with( + name="ValidatorPermit", + block=block, + params=[netuid], + ) + + assert result == expected_validator_permits + + +def test_get_subnet_validator_permits_is_none(subtensor, mocker): + """Test get_subnet_validator_permits returns correct value.""" + # preps + netuid = 14 + block = 123 + + mocked_query_subtensor = mocker.MagicMock(return_value=None) + subtensor.query_subtensor = mocked_query_subtensor + + # call + result = subtensor.get_subnet_validator_permits(netuid=netuid, block=block) + + # asserts + mocked_query_subtensor.assert_called_once_with( + name="ValidatorPermit", + block=block, + params=[netuid], + ) + + assert result is None + + +@pytest.mark.parametrize( + "query_return, expected", + [ + [111, True], + [0, False], + ], +) +def test_is_subnet_active(subtensor, mocker, query_return, expected): + # preps + netuid = mocker.Mock() + block = mocker.Mock() + mocked_query_subtensor = mocker.MagicMock( + return_value=mocker.Mock(value=query_return) + ) + subtensor.query_subtensor = mocked_query_subtensor + + # call + result = subtensor.is_subnet_active(netuid=netuid, block=block) + + # Asserts + mocked_query_subtensor.assert_called_once_with( + name="FirstEmissionBlockNumber", + block=block, + params=[netuid], + ) + + assert result == expected + + +# `geg_l_subnet_info` tests +def test_get_subnet_info_success(mocker, subtensor): + """Test get_subnet_info returns correct data when subnet information is found.""" + # Prep + netuid = mocker.Mock() + block = mocker.Mock() + + mocker.patch.object(subtensor, "query_runtime_api") + mocker.patch.object( + subtensor_module.SubnetInfo, + "from_dict", + ) + + # Call + result = subtensor.get_subnet_info(netuid=netuid, block=block) + + # Asserts + subtensor.query_runtime_api.assert_called_once_with( + runtime_api="SubnetInfoRuntimeApi", + method="get_subnet_info_v2", + params=[netuid], + block=block, + ) + subtensor_module.SubnetInfo.from_dict.assert_called_once_with( + subtensor.query_runtime_api.return_value, + ) + assert result == subtensor_module.SubnetInfo.from_dict.return_value + + +def test_get_subnet_info_no_data(mocker, subtensor): + """Test get_subnet_info returns None.""" + # Prep + netuid = mocker.Mock() + block = mocker.Mock() + mocker.patch.object(subtensor_module.SubnetInfo, "from_dict") + mocker.patch.object(subtensor, "query_runtime_api", return_value=None) + + # Call + result = subtensor.get_subnet_info(netuid=netuid, block=block) + + # Asserts + subtensor.query_runtime_api.assert_called_once_with( + runtime_api="SubnetInfoRuntimeApi", + method="get_subnet_info_v2", + params=[netuid], + block=block, + ) + subtensor_module.SubnetInfo.from_dict.assert_not_called() + assert result is None + + +@pytest.mark.parametrize( + "call_return, expected", + [[10, 111], [None, None], [0, 121]], +) +def test_get_next_epoch_start_block(mocker, subtensor, call_return, expected): + """Check that get_next_epoch_start_block returns the correct value.""" + # Prep + netuid = mocker.Mock() + block = 20 + + mocked_blocks_since_last_step = mocker.Mock(return_value=call_return) + subtensor.blocks_since_last_step = mocked_blocks_since_last_step + + mocker.patch.object(subtensor, "tempo", return_value=100) + + # Call + result = subtensor.get_next_epoch_start_block(netuid=netuid, block=block) + + # Asserts + mocked_blocks_since_last_step.assert_called_once_with( + netuid=netuid, + block=block, + ) + subtensor.tempo.assert_called_once_with(netuid=netuid, block=block) + assert result == expected + + +def test_get_parents_success(subtensor, mocker): + """Tests get_parents when parents are successfully retrieved and formatted.""" + # Preps + fake_hotkey = "valid_hotkey" + fake_netuid = 1 + fake_parents = mocker.Mock( + value=[ + (1000, ["parent_key_1"]), + (2000, ["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"), + ] + + # Call + result = subtensor.get_parents(hotkey=fake_hotkey, netuid=fake_netuid) + + # Asserts + mocked_query.assert_called_once_with( + block_hash=None, + module="SubtensorModule", + 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): + """Tests get_parents when there are no parents to retrieve.""" + # Preps + fake_hotkey = "valid_hotkey" + fake_netuid = 1 + fake_parents = [] + + mocked_query = mocker.MagicMock(return_value=fake_parents) + subtensor.substrate.query = mocked_query + + # Call + result = subtensor.get_parents(hotkey=fake_hotkey, netuid=fake_netuid) + + # Asserts + mocked_query.assert_called_once_with( + block_hash=None, + module="SubtensorModule", + storage_function="ParentKeys", + params=[fake_hotkey, fake_netuid], + ) + assert result == [] + + +def test_set_children(subtensor, fake_wallet, mocker): + """Tests set_children extrinsic calls properly.""" + # Preps + mocked_set_children_extrinsic = mocker.Mock() + mocker.patch.object( + subtensor_module, "set_children_extrinsic", mocked_set_children_extrinsic + ) + fake_children = [ + ( + 1.0, + "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM", + ), + ] + + # Call + result = subtensor.set_children( + fake_wallet, + fake_wallet.hotkey.ss58_address, + netuid=1, + children=fake_children, + ) + + # Asserts + mocked_set_children_extrinsic.assert_called_once_with( + subtensor=subtensor, + wallet=fake_wallet, + hotkey=fake_wallet.hotkey.ss58_address, + netuid=1, + children=fake_children, + wait_for_finalization=True, + wait_for_inclusion=True, + raise_error=False, + period=None, + ) + assert result == mocked_set_children_extrinsic.return_value + + +def test_unstake_all(subtensor, fake_wallet, mocker): + """Verifies unstake_all calls properly.""" + # Preps + fake_unstake_all_extrinsic = mocker.Mock() + mocker.patch.object( + subtensor_module, "unstake_all_extrinsic", fake_unstake_all_extrinsic + ) + # Call + result = subtensor.unstake_all( + wallet=fake_wallet, + hotkey=fake_wallet.hotkey.ss58_address, + netuid=1, + ) + # Asserts + fake_unstake_all_extrinsic.assert_called_once_with( + subtensor=subtensor, + wallet=fake_wallet, + hotkey=fake_wallet.hotkey.ss58_address, + netuid=1, + rate_tolerance=0.005, + wait_for_inclusion=True, + wait_for_finalization=True, + period=None, + raise_error=False, + ) + assert result == fake_unstake_all_extrinsic.return_value + + +def test_get_liquidity_list_subnet_does_not_exits(subtensor, mocker): + """Test get_liquidity_list returns None when subnet doesn't exist.""" + # Preps + mocker.patch.object(subtensor, "subnet_exists", return_value=False) + + # Call + result = subtensor.get_liquidity_list(wallet=mocker.Mock(), netuid=1) + + # Asserts + subtensor.subnet_exists.assert_called_once_with(netuid=1) + assert result is None + + +def test_get_liquidity_list_subnet_is_not_active(subtensor, mocker): + """Test get_liquidity_list returns None when subnet is not active.""" + # Preps + mocker.patch.object(subtensor, "subnet_exists", return_value=True) + mocker.patch.object(subtensor, "is_subnet_active", return_value=False) + + # Call + result = subtensor.get_liquidity_list(wallet=mocker.Mock(), netuid=1) + + # Asserts + subtensor.subnet_exists.assert_called_once_with(netuid=1) + subtensor.is_subnet_active.assert_called_once_with(netuid=1) + assert result is None + + +def test_get_liquidity_list_happy_path(subtensor, fake_wallet, mocker): + """Tests `get_liquidity_list` returns the correct value.""" + netuid = 2 + + # Mock network state + mocker.patch.object(subtensor, "subnet_exists", return_value=True) + mocker.patch.object(subtensor, "is_subnet_active", return_value=True) + mocker.patch.object(subtensor, "determine_block_hash", return_value="0x1234") + + # Mock price and fee calculation + mocker.patch.object(subtensor_module, "price_to_tick", return_value=100) + mocker.patch.object( + subtensor_module, + "calculate_fees", + return_value=(Balance.from_tao(0.0), Balance.from_tao(0.0, netuid)), + ) + + # 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}, + } + ), + ], + ] + mocked_query_map = mocker.MagicMock(return_value=fake_positions) + mocker.patch.object(subtensor, "query_map", new=mocked_query_map) + + # Mock storage key creation + mocker.patch.object( + subtensor.substrate, + "create_storage_key", + side_effect=lambda pallet, + storage_function, + params, + block_hash=None: f"{pallet}:{storage_function}:{params}", + ) + + # Mock query_multi for fee + sqrt_price + tick data + mock_query_multi = mocker.MagicMock( + side_effect=[ + [ + ("key1", {"bits": 0}), # fee_global_tao + ("key2", {"bits": 0}), # fee_global_alpha + ("key3", {"bits": 1072693248}), + ], + [ + ( + "tick_low", + {"fees_out_tao": {"bits": 0}, "fees_out_alpha": {"bits": 0}}, + ), + ( + "tick_high", + {"fees_out_tao": {"bits": 0}, "fees_out_alpha": {"bits": 0}}, + ), + ], + ] + ) + mocker.patch.object(subtensor.substrate, "query_multi", new=mock_query_multi) + + # Call + result = subtensor.get_liquidity_list(wallet=fake_wallet, netuid=netuid) + + # Asserts + assert subtensor.determine_block_hash.call_count == 1 + assert subtensor_module.price_to_tick.call_count == 1 + assert subtensor_module.calculate_fees.call_count == len(fake_positions) + mocked_query_map.assert_called_once_with( + module="Swap", + name="Positions", + block=None, + params=[netuid, fake_wallet.coldkeypub.ss58_address], + ) + assert mock_query_multi.call_count == 2 # one for fees, one for ticks + assert len(result) == len(fake_positions) + assert all(isinstance(p, subtensor_module.LiquidityPosition) for p in result) + + +def test_add_liquidity(subtensor, fake_wallet, mocker): + """Test add_liquidity extrinsic calls properly.""" + # preps + netuid = 123 + mocked_extrinsic = mocker.patch.object(subtensor_module, "add_liquidity_extrinsic") + + # Call + result = subtensor.add_liquidity( + wallet=fake_wallet, + netuid=netuid, + liquidity=Balance.from_tao(150), + price_low=Balance.from_tao(180).rao, + price_high=Balance.from_tao(130).rao, + ) + + # Asserts + mocked_extrinsic.assert_called_once_with( + subtensor=subtensor, + wallet=fake_wallet, + netuid=netuid, + liquidity=Balance.from_tao(150), + price_low=Balance.from_tao(180).rao, + price_high=Balance.from_tao(130).rao, + hotkey=None, + wait_for_inclusion=True, + wait_for_finalization=True, + period=None, + raise_error=False, + ) + assert result == mocked_extrinsic.return_value + + +def test_modify_liquidity(subtensor, fake_wallet, mocker): + """Test modify_liquidity extrinsic calls properly.""" + # preps + netuid = 123 + mocked_extrinsic = mocker.patch.object( + subtensor_module, "modify_liquidity_extrinsic" + ) + position_id = 2 + + # Call + result = subtensor.modify_liquidity( + wallet=fake_wallet, + netuid=netuid, + position_id=position_id, + liquidity_delta=Balance.from_tao(150), + ) + + # Asserts + mocked_extrinsic.assert_called_once_with( + subtensor=subtensor, + wallet=fake_wallet, + netuid=netuid, + position_id=position_id, + liquidity_delta=Balance.from_tao(150), + hotkey=None, + wait_for_inclusion=True, + wait_for_finalization=True, + period=None, + raise_error=False, + ) + assert result == mocked_extrinsic.return_value + + +def test_remove_liquidity(subtensor, fake_wallet, mocker): + """Test remove_liquidity extrinsic calls properly.""" + # preps + netuid = 123 + mocked_extrinsic = mocker.patch.object( + subtensor_module, "remove_liquidity_extrinsic" + ) + position_id = 2 + + # Call + result = subtensor.remove_liquidity( + wallet=fake_wallet, + netuid=netuid, + position_id=position_id, + ) + + # Asserts + mocked_extrinsic.assert_called_once_with( + subtensor=subtensor, + wallet=fake_wallet, + netuid=netuid, + position_id=position_id, + hotkey=None, + wait_for_inclusion=True, + wait_for_finalization=True, + period=None, + raise_error=False, + ) + assert result == mocked_extrinsic.return_value + + +def test_toggle_user_liquidity(subtensor, fake_wallet, mocker): + """Test toggle_user_liquidity extrinsic calls properly.""" + # preps + netuid = 123 + mocked_extrinsic = mocker.patch.object( + subtensor_module, "toggle_user_liquidity_extrinsic" + ) + enable = mocker.Mock() + + # Call + result = subtensor.toggle_user_liquidity( + wallet=fake_wallet, + netuid=netuid, + enable=enable, + ) + + # Asserts + mocked_extrinsic.assert_called_once_with( + subtensor=subtensor, + wallet=fake_wallet, + netuid=netuid, + enable=enable, + wait_for_inclusion=True, + wait_for_finalization=True, + period=None, + raise_error=False, + ) + assert result == mocked_extrinsic.return_value + + +def test_get_subnet_price(subtensor, mocker): + """Test get_subnet_price returns the correct value.""" + # preps + netuid = 123 + mocked_determine_block_hash = mocker.patch.object(subtensor, "determine_block_hash") + fake_price = {"bits": 3155343338053956962} + expected_price = Balance.from_tao(0.029258617) + mocked_query = mocker.patch.object( + subtensor.substrate, "query", return_value=fake_price + ) + + # Call + result = subtensor.get_subnet_price( + netuid=netuid, + ) + + # Asserts + mocked_determine_block_hash.assert_called_once_with(block=None) + mocked_query.assert_called_once_with( + module="Swap", + storage_function="AlphaSqrtPrice", + params=[netuid], + block_hash=mocked_determine_block_hash.return_value, + ) + + assert result == expected_price + + +def test_get_subnet_prices(subtensor, mocker): + """Test get_subnet_prices returns the correct value.""" + # preps + mocked_determine_block_hash = mocker.patch.object(subtensor, "determine_block_hash") + fake_prices = [ + [0, {"bits": 0}], + [1, {"bits": 3155343338053956962}], + ] + expected_prices = {0: Balance.from_tao(1), 1: Balance.from_tao(0.029258617)} + mocked_query_map = mocker.patch.object( + subtensor.substrate, "query_map", return_value=fake_prices + ) + + # Call + result = subtensor.get_subnet_prices() + + # Asserts + mocked_determine_block_hash.assert_called_once_with(block=None) + mocked_query_map.assert_called_once_with( + module="Swap", + storage_function="AlphaSqrtPrice", + block_hash=mocked_determine_block_hash.return_value, + page_size=129, # total number of subnets + ) + assert result == expected_prices + + +def test_all_subnets(subtensor, mocker): + """Verify that `all_subnets` calls proper methods and returns the correct value.""" + # Preps + mocked_determine_block_hash = mocker.patch.object(subtensor, "determine_block_hash") + mocked_di_list_from_dicts = mocker.patch.object( + subtensor_module.DynamicInfo, "list_from_dicts" + ) + mocked_get_subnet_prices = mocker.patch.object( + subtensor, + "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 + ) + + # Call + result = subtensor.all_subnets() + + # Asserts + mocked_determine_block_hash.assert_called_once_with(block=None) + subtensor.substrate.runtime_call.assert_called_once_with( + api="SubnetInfoRuntimeApi", + method="get_all_dynamic_info", + block_hash=mocked_determine_block_hash.return_value, + ) + mocked_get_subnet_prices.assert_called_once() + mocked_di_list_from_dicts.assert_called_once_with( + [ + {"netuid": 0, "price": Balance.from_tao(1)}, + {"netuid": 1, "price": Balance.from_tao(0.029258617)}, + ] + ) + assert result == mocked_di_list_from_dicts.return_value + + +def test_subnet(subtensor, mocker): + """Verify that `subnet` calls proper methods and returns the correct value.""" + # Preps + netuid = 14 + mocked_determine_block_hash = mocker.patch.object(subtensor, "determine_block_hash") + mocked_di_from_dict = mocker.patch.object(subtensor_module.DynamicInfo, "from_dict") + 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) + mocker.patch.object( + subtensor.substrate, "runtime_call", return_value=mocked_runtime_call + ) + + # Call + result = subtensor.subnet(netuid=netuid) + + # Asserts + subtensor.substrate.runtime_call.assert_called_once_with( + api="SubnetInfoRuntimeApi", + method="get_dynamic_info", + params=[netuid], + block_hash=mocked_determine_block_hash.return_value, + ) + mocked_determine_block_hash.assert_called_once_with(block=None) + mocked_get_subnet_price.assert_called_once_with(netuid=netuid, block=None) + mocked_di_from_dict.assert_called_once_with( + {"netuid": netuid, "price": Balance.from_tao(100.0)} + ) + assert result == mocked_di_from_dict.return_value + + +def test_get_stake_operations_fee(subtensor, mocker): + """Verify that `get_stake_operations_fee` calls proper methods and returns the correct value.""" + # Preps + netuid = 1 + amount = Balance.from_rao(100_000_000_000) # 100 Tao + mocked_determine_block_hash = mocker.patch.object(subtensor, "determine_block_hash") + mocked_query_map = mocker.patch.object( + subtensor.substrate, "query", return_value=mocker.Mock(value=196) + ) + + # Call + result = subtensor.get_stake_operations_fee(amount=amount, netuid=netuid) + + # Assert + mocked_determine_block_hash.assert_called_once_with(block=None) + mocked_query_map.assert_called_once_with( + module="Swap", + storage_function="FeeRate", + params=[netuid], + block_hash=mocked_determine_block_hash.return_value, + ) + assert result == Balance.from_rao(299076829).set_unit(netuid) + + +def test_get_stake_add_fee(subtensor, mocker): + """Verify that `get_stake_add_fee` calls proper methods and returns the correct value.""" + # Preps + netuid = mocker.Mock() + amount = mocker.Mock() + mocked_get_stake_operations_fee = mocker.patch.object( + subtensor, "get_stake_operations_fee" + ) + + # Call + result = subtensor.get_stake_add_fee( + amount=amount, + netuid=netuid, + coldkey_ss58=mocker.Mock(), + hotkey_ss58=mocker.Mock(), + ) + + # Asserts + mocked_get_stake_operations_fee.assert_called_once_with( + amount=amount, netuid=netuid, block=None + ) + assert result == mocked_get_stake_operations_fee.return_value + + +def test_get_unstake_fee(subtensor, mocker): + """Verify that `get_unstake_fee` calls proper methods and returns the correct value.""" + # Preps + netuid = mocker.Mock() + amount = mocker.Mock() + mocked_get_stake_operations_fee = mocker.patch.object( + subtensor, "get_stake_operations_fee" + ) + + # Call + result = subtensor.get_unstake_fee( + amount=amount, + netuid=netuid, + coldkey_ss58=mocker.Mock(), + hotkey_ss58=mocker.Mock(), + ) + + # Asserts + mocked_get_stake_operations_fee.assert_called_once_with( + amount=amount, netuid=netuid, block=None + ) + assert result == mocked_get_stake_operations_fee.return_value + + +def test_get_stake_movement_fee(subtensor, mocker): + """Verify that `get_stake_movement_fee` calls proper methods and returns the correct value.""" + # Preps + netuid = mocker.Mock() + amount = mocker.Mock() + mocked_get_stake_operations_fee = mocker.patch.object( + subtensor, "get_stake_operations_fee" + ) + + # Call + result = subtensor.get_stake_movement_fee( + amount=amount, + origin_netuid=netuid, + origin_hotkey_ss58=mocker.Mock(), + origin_coldkey_ss58=mocker.Mock(), + destination_netuid=mocker.Mock(), + destination_hotkey_ss58=mocker.Mock(), + destination_coldkey_ss58=mocker.Mock(), + ) + + # Asserts + mocked_get_stake_operations_fee.assert_called_once_with( + amount=amount, netuid=netuid, block=None + ) + assert result == mocked_get_stake_operations_fee.return_value + + +def test_get_stake_weight(subtensor, mocker): + """Verify that `get_stake_weight` method calls proper methods and returns the correct value.""" + # Preps + netuid = mocker.Mock() + fake_weights = [0, 100, 15000] + expected_result = [0.0, 0.0015259021896696422, 0.22888532845044632] + + mock_determine_block_hash = mocker.patch.object( + subtensor, + "determine_block_hash", + ) + mocked_query = mocker.patch.object( + subtensor.substrate, + "query", + return_value=fake_weights, + ) + + # Call + result = subtensor.get_stake_weight(netuid=netuid) + + # Asserts + mock_determine_block_hash.assert_called_once_with(block=None) + mocked_query.assert_called_once_with( + module="SubtensorModule", + storage_function="StakeWeight", + params=[netuid], + block_hash=mock_determine_block_hash.return_value, + ) + assert result == expected_result + + +def test_get_timelocked_weight_commits(subtensor, mocker): + """Verify that `get_timelocked_weight_commits` method calls proper methods and returns the correct value.""" + # Preps + netuid = mocker.Mock() + + mock_determine_block_hash = mocker.patch.object( + subtensor, + "determine_block_hash", + ) + mocked_query_map = mocker.patch.object( + subtensor.substrate, + "query_map", + ) + + # Call + result = subtensor.get_timelocked_weight_commits(netuid=netuid) + + # Asserts + mock_determine_block_hash.assert_called_once_with(block=None) + mocked_query_map.assert_called_once_with( + module="SubtensorModule", + storage_function="TimelockedWeightCommits", + params=[netuid], + block_hash=mock_determine_block_hash.return_value, + ) + assert result == [] diff --git a/tests/unit_tests/test_subtensor_api.py b/tests/unit_tests/test_subtensor_api.py new file mode 100644 index 0000000000..5007370d96 --- /dev/null +++ b/tests/unit_tests/test_subtensor_api.py @@ -0,0 +1,102 @@ +from bittensor.core.subtensor import Subtensor +from bittensor.core.subtensor_api import SubtensorApi +import pytest + + +def test_properties_methods_comparable(other_class: "Subtensor" = None): + """Verifies that methods in SubtensorApi and its properties contains all Subtensors methods.""" + # Preps + subtensor = ( + other_class(network="latent-lite", _mock=True) + if other_class + else Subtensor(network="latent-lite", _mock=True) + ) + subtensor_api = SubtensorApi(network="latent-lite", mock=True) + + subtensor_methods = [m for m in dir(subtensor) if not m.startswith("_")] + + excluded_subtensor_methods = ["commit"] + + subtensor_api_methods = [m for m in dir(subtensor_api) if not m.startswith("_")] + chain_methods = [m for m in dir(subtensor_api.chain) if not m.startswith("_")] + commitments_methods = [ + m for m in dir(subtensor_api.commitments) if not m.startswith("_") + ] + delegates_methods = [ + m for m in dir(subtensor_api.delegates) if not m.startswith("_") + ] + extrinsics_methods = [ + m for m in dir(subtensor_api.extrinsics) if not m.startswith("_") + ] + metagraphs_methods = [ + m for m in dir(subtensor_api.metagraphs) if not m.startswith("_") + ] + neurons_methods = [m for m in dir(subtensor_api.neurons) if not m.startswith("_")] + queries_methods = [m for m in dir(subtensor_api.queries) if not m.startswith("_")] + stakes_methods = [m for m in dir(subtensor_api.staking) if not m.startswith("_")] + subnets_methods = [m for m in dir(subtensor_api.subnets) if not m.startswith("_")] + wallets_methods = [m for m in dir(subtensor_api.wallets) if not m.startswith("_")] + + all_subtensor_api_methods = ( + subtensor_api_methods + + chain_methods + + commitments_methods + + delegates_methods + + extrinsics_methods + + metagraphs_methods + + neurons_methods + + queries_methods + + stakes_methods + + subnets_methods + + wallets_methods + ) + + # Assertions + for method in subtensor_methods: + # skipp excluded methods + if method in excluded_subtensor_methods: + continue + assert method in all_subtensor_api_methods, ( + f"`Subtensor.{method}`is not present in class `SubtensorApi`." + ) + + +def test__methods_comparable_with_passed_legacy_methods( + other_class: "Subtensor" = None, +): + """Verifies that methods in SubtensorApi contains all Subtensors methods if `legacy_methods=True` is passed.""" + # Preps + subtensor = ( + other_class(network="latent-lite", mock=True) + if other_class + else Subtensor(network="latent-lite", _mock=True) + ) + subtensor_api = SubtensorApi(network="latent-lite", mock=True, legacy_methods=True) + + subtensor_methods = [m for m in dir(subtensor) if not m.startswith("_")] + subtensor_api_methods = [m for m in dir(subtensor_api) if not m.startswith("_")] + + excluded_subtensor_methods = ["commit"] + + # Assertions + for method in subtensor_methods: + # skipp excluded methods + if method in excluded_subtensor_methods: + continue + assert method in subtensor_api_methods, ( + f"`Subtensor.{method}`is not present in class `SubtensorApi`." + ) + + +def test_failed_if_subtensor_has_new_method(): + """Verifies that SubtensorApi fails if Subtensor has a new method.""" + # Preps + + class SubtensorWithNewMethod(Subtensor): + def return_my_id(self): + return id(self) + + # Call and assert + + with pytest.raises(AssertionError): + test_properties_methods_comparable(other_class=SubtensorWithNewMethod) diff --git a/tests/unit_tests/test_subtensor_extended.py b/tests/unit_tests/test_subtensor_extended.py new file mode 100644 index 0000000000..01a365bd31 --- /dev/null +++ b/tests/unit_tests/test_subtensor_extended.py @@ -0,0 +1,1528 @@ +import unittest.mock + +import pytest + +import async_substrate_interface.errors + +from bittensor.core.chain_data.axon_info import AxonInfo +from bittensor.core.chain_data.chain_identity import ChainIdentity +from bittensor.core.chain_data.delegate_info import DelegatedInfo, DelegateInfo +from bittensor.core.chain_data.dynamic_info import DynamicInfo +from bittensor.core.chain_data.neuron_info import NeuronInfo +from bittensor.core.chain_data.neuron_info_lite import NeuronInfoLite +from bittensor.core.chain_data.prometheus_info import PrometheusInfo +from bittensor.core.chain_data.stake_info import StakeInfo +from bittensor.utils import U16_MAX, U64_MAX +from bittensor.utils.balance import Balance +from tests.helpers.helpers import assert_submit_signed_extrinsic +from bittensor.core.extrinsics import move_stake + + +@pytest.fixture +def mock_delegate_info(): + return { + "delegate_ss58": tuple(bytearray(32)), + "total_stake": {}, + "nominators": [], + "owner_ss58": tuple(bytearray(32)), + "take": U16_MAX, + "validator_permits": [], + "registrations": [], + "return_per_1000": 2, + "total_daily_return": 3, + } + + +@pytest.fixture +def mock_dynamic_info(): + return { + "netuid": 0, + "owner_hotkey": tuple(bytearray(32)), + "owner_coldkey": tuple(bytearray(32)), + "subnet_name": (114, 111, 111, 116), + "token_symbol": (206, 164), + "tempo": 100, + "last_step": 4919910, + "blocks_since_last_step": 84234, + "emission": 0, + "alpha_in": 14723086336554, + "alpha_out": 6035890271491007, + "tao_in": 6035892206947246, + "alpha_out_emission": 0, + "alpha_in_emission": 0, + "tao_in_emission": 0, + "pending_alpha_emission": 0, + "pending_root_emission": 0, + "subnet_volume": 2240411565906691, + "network_registered_at": 0, + "subnet_identity": None, + "moving_price": {"bits": 0}, + } + + +@pytest.fixture +def mock_neuron_info(): + return { + "active": 0, + "axon_info": { + "ip_type": 4, + "ip": 2130706433, + "placeholder1": 0, + "placeholder2": 0, + "port": 8080, + "protocol": 0, + "version": 1, + }, + "bonds": [], + "coldkey": tuple(bytearray(32)), + "consensus": 0.0, + "dividends": 0.0, + "emission": 0.0, + "hotkey": tuple(bytearray(32)), + "incentive": 0.0, + "is_null": False, + "last_update": 0, + "netuid": 1, + "prometheus_info": { + "block": 0, + "ip_type": 0, + "ip": 0, + "port": 0, + "version": 1, + }, + "pruning_score": 0.0, + "rank": 0.0, + "stake_dict": {}, + "stake": [], + "total_stake": 1e12, + "trust": 0.0, + "uid": 1, + "validator_permit": True, + "validator_trust": 0.0, + "weights": [], + } + + +def test_all_subnets(mock_substrate, subtensor, mock_dynamic_info): + mock_substrate.runtime_call.return_value.decode.return_value = [ + mock_dynamic_info, + ] + + result = subtensor.all_subnets() + + assert result == [ + DynamicInfo( + netuid=0, + owner_hotkey="5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM", + owner_coldkey="5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM", + subnet_name="root", + symbol="Τ", + tempo=100, + last_step=4919910, + blocks_since_last_step=84234, + emission=Balance(0), + alpha_in=Balance(14723086336554), + alpha_out=Balance(6035890271491007), + tao_in=Balance(6035892206947246), + price=Balance.from_tao(1), + k=88866962081017766138079430284, + is_dynamic=False, + alpha_out_emission=Balance(0), + alpha_in_emission=Balance(0), + tao_in_emission=Balance(0), + pending_alpha_emission=Balance(0), + pending_root_emission=Balance(0), + network_registered_at=0, + subnet_volume=Balance(2240411565906691), + subnet_identity=None, + moving_price=0.0, + ), + ] + + mock_substrate.runtime_call.assert_called_once_with( + "SubnetInfoRuntimeApi", + "get_all_dynamic_info", + block_hash=None, + ) + + +def test_bonds(mock_substrate, subtensor, mocker): + mock_substrate.query_map.return_value = [ + (0, mocker.Mock(value=[(1, 100), (2, 200)])), + (1, mocker.Mock(value=[(0, 150), (2, 250)])), + (2, mocker.Mock(value=None)), + ] + + result = subtensor.bonds(netuid=1) + + assert result == [ + (0, [(1, 100), (2, 200)]), + (1, [(0, 150), (2, 250)]), + ] + + mock_substrate.query_map.assert_called_once_with( + module="SubtensorModule", + storage_function="Bonds", + params=[1], + block_hash=None, + ) + + +def test_burned_register(mock_substrate, subtensor, fake_wallet, mocker): + mock_substrate.get_payment_info.return_value = {"partial_fee": 10} + mocker.patch.object( + subtensor, + "get_neuron_for_pubkey_and_subnet", + return_value=NeuronInfo.get_null_neuron(), + ) + mocker.patch.object(subtensor, "get_balance") + + success = subtensor.burned_register( + fake_wallet, + netuid=1, + ) + + assert success is True + + subtensor.get_neuron_for_pubkey_and_subnet.assert_called_once_with( + fake_wallet.hotkey.ss58_address, + netuid=1, + block=mock_substrate.get_block_number.return_value, + ) + + assert_submit_signed_extrinsic( + mock_substrate, + fake_wallet.coldkey, + call_module="SubtensorModule", + call_function="burned_register", + call_params={ + "netuid": 1, + "hotkey": fake_wallet.hotkey.ss58_address, + }, + wait_for_finalization=True, + wait_for_inclusion=True, + ) + + +def test_burned_register_on_root(mock_substrate, subtensor, fake_wallet, mocker): + mocker.patch.object( + subtensor, + "get_balance", + return_value=Balance(1), + ) + mocker.patch.object( + subtensor, + "is_hotkey_registered", + return_value=False, + ) + + success = subtensor.burned_register( + fake_wallet, + netuid=0, + ) + + assert success is True + + subtensor.is_hotkey_registered.assert_called_once_with( + netuid=0, + hotkey_ss58=fake_wallet.hotkey.ss58_address, + ) + + assert_submit_signed_extrinsic( + mock_substrate, + fake_wallet.coldkey, + call_module="SubtensorModule", + call_function="root_register", + call_params={ + "hotkey": fake_wallet.hotkey.ss58_address, + }, + wait_for_finalization=True, + wait_for_inclusion=True, + ) + + +def test_get_all_commitments(mock_substrate, subtensor, mocker): + mock_substrate.query_map.return_value = [ + ( + (tuple(bytearray(32)),), + { + "info": { + "fields": [ + ( + { + "Raw4": (tuple(b"Test"),), + }, + ), + ], + }, + }, + ), + ] + + result = subtensor.get_all_commitments(netuid=1) + + assert result == { + "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM": "Test", + } + + mock_substrate.query_map.assert_called_once_with( + module="Commitments", + storage_function="CommitmentOf", + params=[1], + block_hash=None, + ) + + +def test_get_balance(mock_substrate, subtensor): + mock_substrate.query.return_value = { + "data": { + "free": 123, + }, + } + + result = subtensor.get_balance( + "hotkey_ss58", + ) + + assert result == Balance(123) + + mock_substrate.query.assert_called_once_with( + module="System", + storage_function="Account", + params=["hotkey_ss58"], + block_hash=None, + ) + + +def test_get_balances(mock_substrate, subtensor, mocker): + create_storage_keys = [ + mocker.Mock(), + mocker.Mock(), + ] + + mock_substrate.create_storage_key.side_effect = create_storage_keys + mock_substrate.query_multi.return_value = [ + ( + mocker.Mock( + params=["hotkey1_ss58"], + ), + { + "data": { + "free": 1, + }, + }, + ), + ( + mocker.Mock( + params=["hotkey2_ss58"], + ), + { + "data": { + "free": 2, + }, + }, + ), + ] + + result = subtensor.get_balances( + "hotkey1_ss58", + "hotkey2_ss58", + ) + + assert result == { + "hotkey1_ss58": Balance(1), + "hotkey2_ss58": Balance(2), + } + + mock_substrate.query_multi.assert_called_once_with( + create_storage_keys, + block_hash=mock_substrate.get_chain_head.return_value, + ) + mock_substrate.create_storage_key.assert_has_calls( + [ + mocker.call( + "System", + "Account", + ["hotkey1_ss58"], + block_hash=mock_substrate.get_chain_head.return_value, + ), + mocker.call( + "System", + "Account", + ["hotkey2_ss58"], + block_hash=mock_substrate.get_chain_head.return_value, + ), + ] + ) + + +def test_get_block_hash_none(mock_substrate, subtensor): + result = subtensor.get_block_hash(block=None) + + assert result == mock_substrate.get_chain_head.return_value + + mock_substrate.get_chain_head.assert_called_once() + + +def test_get_children(mock_substrate, subtensor, fake_wallet): + mock_substrate.query.return_value.value = [ + ( + U64_MAX, + (tuple(bytearray(32)),), + ), + ] + + success, children, error = subtensor.get_children( + "hotkey_ss58", + netuid=1, + ) + + assert success is True + assert children == [ + ( + 1.0, + "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM", + ), + ] + assert error == "" + + mock_substrate.query.assert_called_once_with( + module="SubtensorModule", + storage_function="ChildKeys", + params=["hotkey_ss58", 1], + block_hash=None, + ) + + +def test_get_children_pending(mock_substrate, subtensor): + mock_substrate.query.return_value.value = [ + [ + ( + U64_MAX, + (tuple(bytearray(32)),), + ), + ], + 123, + ] + + children, cooldown = subtensor.get_children_pending( + "hotkey_ss58", + netuid=1, + ) + + assert children == [ + ( + 1.0, + "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM", + ), + ] + assert cooldown == 123 + + mock_substrate.query.assert_called_once_with( + module="SubtensorModule", + storage_function="PendingChildKeys", + params=[1, "hotkey_ss58"], + block_hash=None, + ) + + +def test_get_current_weight_commit_info_v2( + mock_substrate, subtensor, fake_wallet, mocker +): + mock_substrate.query_map.return_value.records = [ + ( + mocker.ANY, + [ + ( + bytearray(32), + 100, + b"data", + 123, + ), + ], + ), + ] + + result = subtensor.get_current_weight_commit_info_v2( + netuid=1, + ) + + assert result == [ + ( + "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM", + 100, + "0x64617461", + 123, + ), + ] + + mock_substrate.query_map.assert_called_once_with( + module="SubtensorModule", + storage_function="CRV3WeightCommitsV2", + params=[1], + block_hash=None, + ) + + +def test_get_delegate_by_hotkey(mock_substrate, subtensor, mock_delegate_info): + mock_substrate.runtime_call.return_value.value = mock_delegate_info + + result = subtensor.get_delegate_by_hotkey( + "hotkey_ss58", + ) + + assert result == DelegateInfo( + hotkey_ss58="5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM", + owner_ss58="5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM", + take=1.0, + validator_permits=[], + registrations=[], + return_per_1000=Balance(2), + total_daily_return=Balance(3), + total_stake={}, + nominators={}, + ) + + mock_substrate.runtime_call.assert_called_once_with( + "DelegateInfoRuntimeApi", + "get_delegate", + ["hotkey_ss58"], + None, + ) + + +def test_get_delegate_identities(mock_substrate, subtensor, mocker): + mock_substrate.query_map.return_value = [ + ( + (tuple(bytearray(32)),), + mocker.Mock( + value={ + "additional": "Additional", + "description": "Description", + "discord": "", + "github_repo": "https://github.com/opentensor/bittensor", + "image": "", + "name": "Chain Delegate", + "url": "https://www.example.com", + }, + ), + ), + ] + + result = subtensor.get_delegate_identities() + + assert result == { + "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM": ChainIdentity( + additional="Additional", + description="Description", + discord="", + github="https://github.com/opentensor/bittensor", + image="", + name="Chain Delegate", + url="https://www.example.com", + ), + } + + +def test_get_delegated(mock_substrate, subtensor, mock_delegate_info): + mock_substrate.runtime_call.return_value.value = [ + ( + mock_delegate_info, + ( + 0, + 999, + ), + ), + ] + + result = subtensor.get_delegated( + "coldkey_ss58", + ) + + assert result == [ + DelegatedInfo( + hotkey_ss58="5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM", + owner_ss58="5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM", + take=1.0, + validator_permits=[], + registrations=[], + return_per_1000=Balance(2), + total_daily_return=Balance(3), + netuid=0, + stake=Balance(999), + ), + ] + + mock_substrate.runtime_call.assert_called_once_with( + "DelegateInfoRuntimeApi", + "get_delegated", + ["coldkey_ss58"], + None, + ) + + +def test_get_neuron_certificate(mock_substrate, subtensor): + mock_substrate.query.return_value = { + "public_key": (tuple(b"CERTDATA"),), + "algorithm": 63, + } + + result = subtensor.get_neuron_certificate( + "hotkey_ss58", + netuid=1, + ) + + assert result == "?CERTDATA" + + mock_substrate.query.assert_called_once_with( + module="SubtensorModule", + storage_function="NeuronCertificates", + params=[1, "hotkey_ss58"], + block_hash=None, + ) + + +def test_get_stake_for_coldkey(mock_substrate, subtensor, mocker): + mock_substrate.runtime_call.return_value.value = [ + { + "coldkey": tuple(bytearray(32)), + "drain": 0, + "emission": 3, + "hotkey": tuple(bytearray(32)), + "is_registered": True, + "locked": 2, + "netuid": 1, + "stake": 999, + }, + # filter out (stake=0): + { + "coldkey": tuple(bytearray(32)), + "drain": 1000, + "emission": 1000, + "hotkey": tuple(bytearray(32)), + "is_registered": True, + "locked": 1000, + "netuid": 2, + "stake": 0, + }, + ] + + result = subtensor.get_stake_for_coldkey( + "coldkey_ss58", + ) + + assert result == [ + StakeInfo( + coldkey_ss58="5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM", + drain=0, + emission=Balance(3), + hotkey_ss58="5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM", + is_registered=True, + locked=Balance(2), + netuid=1, + stake=Balance(999), + ), + ] + + mock_substrate.runtime_call.assert_called_once_with( + "StakeInfoRuntimeApi", + "get_stake_info_for_coldkey", + ["coldkey_ss58"], + None, + ) + + +def test_filter_netuids_by_registered_hotkeys( + mock_substrate, subtensor, fake_wallet, mocker +): + mock_substrate.query_map.return_value = mocker.MagicMock( + **{ + "__iter__.return_value": iter( + [ + ( + 2, + mocker.Mock( + value=1, + ), + ), + ( + 3, + mocker.Mock( + value=1, + ), + ), + ] + ), + }, + ) + + result = subtensor.filter_netuids_by_registered_hotkeys( + all_netuids=[0, 1, 2], + filter_for_netuids=[2], + all_hotkeys=[fake_wallet], + block=10, + ) + + assert result == [2] + + mock_substrate.get_block_hash.assert_called_once_with(10) + mock_substrate.query_map.assert_called_once_with( + module="SubtensorModule", + storage_function="IsNetworkMember", + params=[fake_wallet.hotkey.ss58_address], + block_hash=mock_substrate.get_block_hash.return_value, + ) + + +def test_last_drand_round(mock_substrate, subtensor): + mock_substrate.query.return_value.value = 123 + + result = subtensor.last_drand_round() + + assert result == 123 + + mock_substrate.query.assert_called_once_with( + module="Drand", + storage_function="LastStoredRound", + ) + + +@pytest.mark.parametrize( + "wait", + ( + True, + False, + ), +) +def test_move_stake(mock_substrate, subtensor, fake_wallet, wait): + success = subtensor.move_stake( + wallet=fake_wallet, + origin_hotkey_ss58="origin_hotkey", + origin_netuid=1, + destination_hotkey_ss58="destination_hotkey", + destination_netuid=2, + amount=Balance(1), + wait_for_finalization=wait, + wait_for_inclusion=wait, + ) + + assert success is True + + assert_submit_signed_extrinsic( + mock_substrate, + fake_wallet.coldkey, + call_module="SubtensorModule", + call_function="move_stake", + call_params={ + "origin_hotkey": "origin_hotkey", + "origin_netuid": 1, + "destination_hotkey": "destination_hotkey", + "destination_netuid": 2, + "alpha_amount": 1, + }, + wait_for_finalization=wait, + wait_for_inclusion=wait, + ) + + +def test_move_stake_insufficient_stake(mock_substrate, subtensor, fake_wallet, mocker): + mocker.patch.object(subtensor, "get_stake", return_value=Balance(0)) + + success = subtensor.move_stake( + fake_wallet, + origin_hotkey_ss58="origin_hotkey", + origin_netuid=1, + destination_hotkey_ss58="destination_hotkey", + destination_netuid=2, + amount=Balance(1), + ) + + assert success is False + + mock_substrate.submit_extrinsic.assert_not_called() + + +def test_move_stake_error(mock_substrate, subtensor, fake_wallet, mocker): + mock_substrate.submit_extrinsic.return_value = mocker.Mock( + error_message="ERROR", + is_success=False, + ) + + success = subtensor.move_stake( + fake_wallet, + origin_hotkey_ss58="origin_hotkey", + origin_netuid=1, + destination_hotkey_ss58="destination_hotkey", + destination_netuid=2, + amount=Balance(1), + ) + + assert success is False + + assert_submit_signed_extrinsic( + mock_substrate, + fake_wallet.coldkey, + call_module="SubtensorModule", + call_function="move_stake", + call_params={ + "origin_hotkey": "origin_hotkey", + "origin_netuid": 1, + "destination_hotkey": "destination_hotkey", + "destination_netuid": 2, + "alpha_amount": 1, + }, + wait_for_finalization=True, + wait_for_inclusion=True, + ) + + +def test_move_stake_exception(mock_substrate, subtensor, fake_wallet): + mock_substrate.submit_extrinsic.side_effect = RuntimeError + + success = subtensor.move_stake( + fake_wallet, + origin_hotkey_ss58="origin_hotkey", + origin_netuid=1, + destination_hotkey_ss58="destination_hotkey", + destination_netuid=2, + amount=Balance(1), + ) + + assert success is False + + assert_submit_signed_extrinsic( + mock_substrate, + fake_wallet.coldkey, + call_module="SubtensorModule", + call_function="move_stake", + call_params={ + "origin_hotkey": "origin_hotkey", + "origin_netuid": 1, + "destination_hotkey": "destination_hotkey", + "destination_netuid": 2, + "alpha_amount": 1, + }, + wait_for_finalization=True, + wait_for_inclusion=True, + ) + + +def test_neurons(mock_substrate, subtensor, mock_neuron_info): + mock_substrate.runtime_call.return_value.value = [ + mock_neuron_info, + ] + + neurons = subtensor.neurons(netuid=1) + + assert neurons == [ + NeuronInfo( + axon_info=AxonInfo( + version=1, + ip="127.0.0.1", + port=8080, + ip_type=4, + hotkey="5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM", + coldkey="5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM", + ), + active=0, + bonds=[], + coldkey="5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM", + consensus=0.0, + dividends=0.0, + emission=0.0, + hotkey="5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM", + incentive=0.0, + is_null=False, + last_update=0, + netuid=1, + prometheus_info=PrometheusInfo( + block=0, + version=1, + ip="0.0.0.0", + port=0, + ip_type=0, + ), + pruning_score=0.0, + rank=0.0, + stake_dict={}, + stake=Balance(0), + total_stake=Balance(0), + trust=0.0, + uid=1, + validator_permit=True, + validator_trust=0.0, + weights=[], + ), + ] + + mock_substrate.runtime_call.assert_called_once_with( + "NeuronInfoRuntimeApi", + "get_neurons", + [1], + None, + ) + + +def test_neurons_lite(mock_substrate, subtensor, mock_neuron_info): + mock_substrate.runtime_call.return_value.value = [ + mock_neuron_info, + ] + + result = subtensor.neurons_lite(netuid=1) + + assert result == [ + NeuronInfoLite( + axon_info=AxonInfo( + version=1, + ip="127.0.0.1", + port=8080, + ip_type=4, + hotkey="5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM", + coldkey="5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM", + ), + active=0, + coldkey="5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM", + consensus=0.0, + dividends=0.0, + emission=0.0, + hotkey="5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM", + incentive=0.0, + is_null=False, + last_update=0, + netuid=1, + prometheus_info=PrometheusInfo( + block=0, + version=1, + ip="0.0.0.0", + port=0, + ip_type=0, + ), + pruning_score=0.0, + rank=0.0, + stake_dict={}, + stake=Balance(0), + total_stake=Balance(0), + trust=0.0, + uid=1, + validator_permit=True, + validator_trust=0.0, + ), + ] + + mock_substrate.runtime_call.assert_called_once_with( + "NeuronInfoRuntimeApi", + "get_neurons_lite", + [1], + None, + ) + + +def test_set_children(mock_substrate, subtensor, fake_wallet, mocker): + subtensor.set_children( + fake_wallet, + fake_wallet.hotkey.ss58_address, + netuid=1, + children=[ + ( + 1.0, + "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM", + ), + ], + ) + + assert_submit_signed_extrinsic( + mock_substrate, + fake_wallet.coldkey, + call_module="SubtensorModule", + call_function="set_children", + call_params={ + "children": [ + ( + U64_MAX, + "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM", + ) + ], + "hotkey": fake_wallet.hotkey.ss58_address, + "netuid": 1, + }, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + +def test_set_delegate_take_equal(mock_substrate, subtensor, fake_wallet, mocker): + mocker.patch.object(subtensor, "get_delegate_take", return_value=0.18) + + subtensor.set_delegate_take( + fake_wallet, + fake_wallet.hotkey.ss58_address, + 0.18, + ) + + mock_substrate.submit_extrinsic.assert_not_called() + + +def test_set_delegate_take_increase(mock_substrate, subtensor, fake_wallet, mocker): + mocker.patch.object(subtensor, "get_delegate_take", return_value=0.18) + + subtensor.set_delegate_take( + fake_wallet, + fake_wallet.hotkey.ss58_address, + 0.2, + ) + + assert_submit_signed_extrinsic( + mock_substrate, + fake_wallet.coldkey, + call_module="SubtensorModule", + call_function="increase_take", + call_params={ + "hotkey": fake_wallet.hotkey.ss58_address, + "take": 13107, + }, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + +def test_set_delegate_take_decrease(mock_substrate, subtensor, fake_wallet, mocker): + mocker.patch.object(subtensor, "get_delegate_take", return_value=0.18) + + subtensor.set_delegate_take( + fake_wallet, + fake_wallet.hotkey.ss58_address, + 0.1, + ) + + assert_submit_signed_extrinsic( + mock_substrate, + fake_wallet.coldkey, + call_module="SubtensorModule", + call_function="decrease_take", + call_params={ + "hotkey": fake_wallet.hotkey.ss58_address, + "take": 6553, + }, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + +def test_subnet(mock_substrate, subtensor, mock_dynamic_info): + mock_substrate.runtime_call.return_value.decode.return_value = mock_dynamic_info + + result = subtensor.subnet(netuid=0) + + assert result == DynamicInfo( + netuid=0, + owner_hotkey="5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM", + owner_coldkey="5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM", + subnet_name="root", + symbol="Τ", + tempo=100, + last_step=4919910, + blocks_since_last_step=84234, + emission=Balance(0), + alpha_in=Balance(14723086336554), + alpha_out=Balance(6035890271491007), + tao_in=Balance(6035892206947246), + price=Balance.from_tao(1), + k=88866962081017766138079430284, + is_dynamic=False, + alpha_out_emission=Balance(0), + alpha_in_emission=Balance(0), + tao_in_emission=Balance(0), + pending_alpha_emission=Balance(0), + pending_root_emission=Balance(0), + network_registered_at=0, + subnet_volume=Balance(2240411565906691), + subnet_identity=None, + moving_price=0.0, + ) + + mock_substrate.runtime_call.assert_called_once_with( + "SubnetInfoRuntimeApi", + "get_dynamic_info", + params=[0], + block_hash=None, + ) + + +def test_subtensor_contextmanager(mock_substrate, subtensor): + with subtensor: + pass + + mock_substrate.close.assert_called_once() + + +def test_swap_stake(mock_substrate, subtensor, fake_wallet, mocker): + mocker.patch.object(subtensor, "get_stake", return_value=Balance(1000)) + mocker.patch.object( + subtensor, + "get_hotkey_owner", + autospec=True, + return_value=fake_wallet.coldkeypub.ss58_address, + ) + + result = subtensor.swap_stake( + fake_wallet, + fake_wallet.hotkey.ss58_address, + origin_netuid=1, + destination_netuid=2, + amount=Balance(999), + ) + + assert result is True + + assert_submit_signed_extrinsic( + mock_substrate, + fake_wallet.coldkey, + call_module="SubtensorModule", + call_function="swap_stake", + call_params={ + "hotkey": fake_wallet.hotkey.ss58_address, + "origin_netuid": 1, + "destination_netuid": 2, + "alpha_amount": 999, + }, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + +@pytest.mark.parametrize( + "query,result", + ( + ( + None, + None, + ), + ( + { + "additional": "Additional", + "description": "Description", + "discord": "", + "github_repo": "https://github.com/opentensor/bittensor", + "image": "", + "name": "Chain Delegate", + "url": "https://www.example.com", + }, + ChainIdentity( + additional="Additional", + description="Description", + discord="", + github="https://github.com/opentensor/bittensor", + image="", + name="Chain Delegate", + url="https://www.example.com", + ), + ), + ), +) +def test_query_identity(mock_substrate, subtensor, query, result): + mock_substrate.query.return_value = query + + identity = subtensor.query_identity( + "coldkey_ss58", + ) + + assert identity == result + + mock_substrate.query.assert_called_once_with( + module="SubtensorModule", + storage_function="IdentitiesV2", + params=["coldkey_ss58"], + block_hash=None, + ) + + +def test_register(mock_substrate, subtensor, fake_wallet, mocker): + create_pow = mocker.patch( + "bittensor.core.extrinsics.registration.create_pow", + return_value=mocker.Mock( + **{ + "is_stale.return_value": False, + "seal": b"\1\2\3", + }, + ), + ) + mocker.patch.object( + subtensor, + "get_neuron_for_pubkey_and_subnet", + return_value=NeuronInfo.get_null_neuron(), + ) + + result = subtensor.register( + fake_wallet, + netuid=1, + ) + + assert result is True + + subtensor.get_neuron_for_pubkey_and_subnet.assert_called_once_with( + hotkey_ss58=fake_wallet.hotkey.ss58_address, + netuid=1, + block=mock_substrate.get_block_number.return_value, + ) + create_pow.assert_called_once_with( + subtensor=subtensor, + wallet=fake_wallet, + netuid=1, + output_in_place=True, + cuda=False, + num_processes=None, + update_interval=None, + log_verbose=False, + ) + + assert_submit_signed_extrinsic( + mock_substrate, + fake_wallet.coldkey, + call_module="SubtensorModule", + call_function="register", + call_params={ + "block_number": create_pow.return_value.block_number, + "coldkey": fake_wallet.coldkeypub.ss58_address, + "hotkey": fake_wallet.hotkey.ss58_address, + "netuid": 1, + "nonce": create_pow.return_value.nonce, + "work": [1, 2, 3], + }, + ) + + +@pytest.mark.parametrize( + "success", + [ + True, + False, + ], +) +def test_register_subnet(mock_substrate, subtensor, fake_wallet, mocker, success): + mocker.patch.object(subtensor, "get_balance", return_value=Balance(100)) + mocker.patch.object(subtensor, "get_subnet_burn_cost", return_value=Balance(10)) + + mock_substrate.submit_extrinsic.return_value = mocker.Mock( + is_success=success, + ) + + result = subtensor.register_subnet(fake_wallet) + + assert result is success + + assert_submit_signed_extrinsic( + substrate=mock_substrate, + keypair=fake_wallet.coldkey, + call_module="SubtensorModule", + call_function="register_network", + call_params={ + "hotkey": fake_wallet.hotkey.ss58_address, + "mechid": 1, + }, + ) + + +def test_register_subnet_insufficient_funds( + mock_substrate, subtensor, fake_wallet, mocker +): + mocker.patch.object(subtensor, "get_balance", return_value=Balance(0)) + mocker.patch.object(subtensor, "get_subnet_burn_cost", return_value=Balance(10)) + + success = subtensor.register_subnet(fake_wallet) + + assert success is False + + mock_substrate.submit_extrinsic.assert_not_called() + + +def test_root_register(mock_substrate, subtensor, fake_wallet, mocker): + mocker.patch.object( + subtensor, "get_balance", autospec=True, return_value=Balance(100) + ) + mocker.patch.object(subtensor, "get_hyperparameter", autospec=True, return_value=10) + mocker.patch.object( + subtensor, "is_hotkey_registered_on_subnet", autospec=True, return_value=False + ) + + success = subtensor.root_register(fake_wallet) + + assert success is True + + subtensor.get_balance.assert_called_once_with( + fake_wallet.coldkeypub.ss58_address, + block=mock_substrate.get_block_number.return_value, + ) + subtensor.get_hyperparameter.assert_called_once() + subtensor.is_hotkey_registered_on_subnet.assert_called_once_with( + fake_wallet.hotkey.ss58_address, + 0, + None, + ) + + assert_submit_signed_extrinsic( + substrate=mock_substrate, + keypair=fake_wallet.coldkey, + call_module="SubtensorModule", + call_function="root_register", + call_params={ + "hotkey": fake_wallet.hotkey.ss58_address, + }, + ) + + +def test_root_register_is_already_registered( + mock_substrate, subtensor, fake_wallet, mocker +): + mocker.patch.object( + subtensor, "get_balance", autospec=True, return_value=Balance(100) + ) + mocker.patch.object(subtensor, "get_hyperparameter", autospec=True, return_value=10) + mocker.patch.object( + subtensor, "is_hotkey_registered_on_subnet", autospec=True, return_value=True + ) + + success = subtensor.root_register(fake_wallet) + + assert success is True + + subtensor.is_hotkey_registered_on_subnet.assert_called_once_with( + fake_wallet.hotkey.ss58_address, + 0, + None, + ) + mock_substrate.submit_extrinsic.assert_not_called() + + +def test_sign_and_send_extrinsic(mock_substrate, subtensor, fake_wallet, mocker): + call = mocker.Mock() + + subtensor.sign_and_send_extrinsic( + call, + fake_wallet, + use_nonce=True, + period=10, + ) + + mock_substrate.get_account_next_index.assert_called_once_with( + fake_wallet.hotkey.ss58_address, + ) + mock_substrate.create_signed_extrinsic.assert_called_once_with( + call=call, + era={ + "period": 10, + }, + keypair=fake_wallet.coldkey, + nonce=mock_substrate.get_account_next_index.return_value, + ) + mock_substrate.submit_extrinsic.assert_called_once_with( + mock_substrate.create_signed_extrinsic.return_value, + wait_for_inclusion=True, + wait_for_finalization=False, + ) + + +def test_sign_and_send_extrinsic_raises_error( + mock_substrate, subtensor, fake_wallet, mocker +): + mock_substrate.submit_extrinsic.return_value = mocker.Mock( + error_message={ + "name": "Exception", + }, + is_success=False, + ) + + with pytest.raises( + async_substrate_interface.errors.SubstrateRequestException, + match="{'name': 'Exception'}", + ): + subtensor.sign_and_send_extrinsic( + call=mocker.Mock(), + wallet=fake_wallet, + raise_error=True, + ) + + +@pytest.mark.parametrize( + "wait", + ( + True, + False, + ), +) +def test_transfer_stake(mock_substrate, subtensor, fake_wallet, mocker, wait): + mocker.patch.object( + subtensor, + "get_hotkey_owner", + autospec=True, + return_value=fake_wallet.coldkeypub.ss58_address, + ) + + success = subtensor.transfer_stake( + fake_wallet, + "dest", + "hotkey_ss58", + origin_netuid=1, + destination_netuid=1, + amount=Balance(1), + wait_for_finalization=wait, + wait_for_inclusion=wait, + ) + + assert success is True + + assert_submit_signed_extrinsic( + mock_substrate, + fake_wallet.coldkey, + call_module="SubtensorModule", + call_function="transfer_stake", + call_params={ + "destination_coldkey": "dest", + "hotkey": "hotkey_ss58", + "origin_netuid": 1, + "destination_netuid": 1, + "alpha_amount": 1, + }, + wait_for_finalization=wait, + wait_for_inclusion=wait, + ) + + +@pytest.mark.parametrize( + "side_effect", + ( + ( + unittest.mock.Mock( + error_message="ERROR", + is_success=False, + ), + ), + RuntimeError, + ), +) +def test_transfer_stake_error( + mock_substrate, subtensor, fake_wallet, mocker, side_effect +): + mocker.patch.object( + subtensor, + "get_hotkey_owner", + autospec=True, + return_value=fake_wallet.coldkeypub.ss58_address, + ) + mock_substrate.submit_extrinsic.return_value = side_effect + + success = subtensor.transfer_stake( + fake_wallet, + "dest", + "hotkey_ss58", + origin_netuid=1, + destination_netuid=1, + amount=Balance(1), + ) + + assert success is False + + assert_submit_signed_extrinsic( + mock_substrate, + fake_wallet.coldkey, + call_module="SubtensorModule", + call_function="transfer_stake", + call_params={ + "destination_coldkey": "dest", + "hotkey": "hotkey_ss58", + "origin_netuid": 1, + "destination_netuid": 1, + "alpha_amount": 1, + }, + wait_for_finalization=True, + wait_for_inclusion=True, + ) + + +def test_transfer_stake_insufficient_stake( + mock_substrate, subtensor, fake_wallet, mocker +): + mocker.patch.object( + subtensor, + "get_hotkey_owner", + autospec=True, + return_value=fake_wallet.coldkeypub.ss58_address, + ) + + with unittest.mock.patch.object( + subtensor, + "get_stake", + return_value=Balance(0), + ): + success = subtensor.transfer_stake( + fake_wallet, + "dest", + "hotkey_ss58", + origin_netuid=1, + destination_netuid=1, + amount=Balance(1), + ) + + assert success is False + + mock_substrate.submit_extrinsic.assert_not_called() + + +def test_wait_for_block(mock_substrate, subtensor, mocker): + mock_subscription_handler = None + + def get_block_handler( + current_block_hash, + header_only, + subscription_handler, + ): + nonlocal mock_subscription_handler + mock_subscription_handler = mocker.Mock(wraps=subscription_handler) + + for block in range(1, 20): + if mock_subscription_handler( + { + "header": { + "number": block, + }, + } + ): + return + + assert False + + mock_substrate.get_block.side_effect = [ + { + "header": { + "number": 1, + }, + }, + ] + mock_substrate.get_block_handler.side_effect = get_block_handler + + subtensor.wait_for_block(block=9) + + assert mock_subscription_handler.call_count == 9 + + +def test_weights(mock_substrate, subtensor): + mock_substrate.query_map.return_value = [ + (1, unittest.mock.Mock(value=0.5)), + ] + + results = subtensor.weights( + netuid=1, + ) + + assert results == [ + ( + 1, + 0.5, + ), + ] + + mock_substrate.query_map.assert_called_once_with( + module="SubtensorModule", + storage_function="Weights", + params=[1], + block_hash=None, + ) diff --git a/tests/unit_tests/test_synapse.py b/tests/unit_tests/test_synapse.py new file mode 100644 index 0000000000..72acddbcb2 --- /dev/null +++ b/tests/unit_tests/test_synapse.py @@ -0,0 +1,252 @@ +import base64 +import json +from typing import Optional, ClassVar + +import pytest + +from bittensor.core.synapse import Synapse + + +def test_parse_headers_to_inputs(): + class Test(Synapse): + key1: list[int] + + # Define a mock headers dictionary to use for testing + headers = { + "bt_header_axon_nonce": "111", + "bt_header_dendrite_ip": "12.1.1.2", + "bt_header_input_obj_key1": base64.b64encode( + json.dumps([1, 2, 3, 4]).encode("utf-8") + ).decode("utf-8"), + "timeout": "12", + "name": "Test", + "header_size": "111", + "total_size": "111", + "computed_body_hash": "0xabcdef", + } + print(headers) + + # Run the function to test + inputs_dict = Test.parse_headers_to_inputs(headers) + print(inputs_dict) + # Check the resulting dictionary + assert inputs_dict == { + "axon": {"nonce": "111"}, + "dendrite": {"ip": "12.1.1.2"}, + "key1": [1, 2, 3, 4], + "timeout": "12", + "name": "Test", + "header_size": "111", + "total_size": "111", + "computed_body_hash": "0xabcdef", + } + + +def test_from_headers(): + class Test(Synapse): + key1: list[int] + + # Define a mock headers dictionary to use for testing + headers = { + "bt_header_axon_nonce": "111", + "bt_header_dendrite_ip": "12.1.1.2", + "bt_header_input_obj_key1": base64.b64encode( + json.dumps([1, 2, 3, 4]).encode("utf-8") + ).decode("utf-8"), + "timeout": "12", + "name": "Test", + "header_size": "111", + "total_size": "111", + "computed_body_hash": "0xabcdef", + } + + # Run the function to test + synapse = Test.from_headers(headers) + + # Check that the resulting object is an instance of YourClass + assert isinstance(synapse, Test) + + # Check the properties of the resulting object + # Replace with actual checks based on the structure of your class + assert synapse.axon.nonce == 111 + assert synapse.dendrite.ip == "12.1.1.2" + assert synapse.key1 == [1, 2, 3, 4] + assert synapse.timeout == 12 + assert synapse.name == "Test" + assert synapse.header_size == 111 + assert synapse.total_size == 111 + + +def test_synapse_create(): + # Create an instance of Synapse + synapse = Synapse() + + # Ensure the instance created is of type Synapse + assert isinstance(synapse, Synapse) + + # Check default properties of a newly created Synapse + assert synapse.name == "Synapse" + assert synapse.timeout == 12.0 + assert synapse.header_size == 0 + assert synapse.total_size == 0 + + # Convert the Synapse instance to a headers dictionary + headers = synapse.to_headers() + + # Ensure the headers is a dictionary and contains the expected keys + assert isinstance(headers, dict) + assert "timeout" in headers + assert "name" in headers + assert "header_size" in headers + assert "total_size" in headers + + # Ensure the 'name' and 'timeout' values match the Synapse's properties + assert headers["name"] == "Synapse" + assert headers["timeout"] == "12.0" + + # Create a new Synapse from the headers and check its 'timeout' property + next_synapse = synapse.from_headers(synapse.to_headers()) + assert next_synapse.timeout == 12.0 + + +def test_custom_synapse(): + # Define a custom Synapse subclass + class Test(Synapse): + a: int # Carried through because required. + b: int = None # Not carried through headers + c: Optional[int] # Required, carried through headers, cannot be None + d: Optional[list[int]] # Required, carried though headers, cannot be None + e: list[int] # Carried through headers + f: Optional[int] = ( + None # Not Required, Not carried through headers, can be None + ) + g: Optional[list[int]] = ( + None # Not Required, Not carried though headers, can be None + ) + + # Create an instance of the custom Synapse subclass + synapse = Test( + a=1, + c=3, + d=[1, 2, 3, 4], + e=[1, 2, 3, 4], + ) + + # Ensure the instance created is of type Test and has the expected properties + assert isinstance(synapse, Test) + assert synapse.name == "Test" + assert synapse.a == 1 + assert synapse.b is None + assert synapse.c == 3 + assert synapse.d == [1, 2, 3, 4] + assert synapse.e == [1, 2, 3, 4] + assert synapse.f is None + assert synapse.g is None + + # Convert the Test instance to a headers dictionary + headers = synapse.to_headers() + + # Ensure the headers contains 'a' but not 'b' + assert "bt_header_input_obj_a" in headers + assert "bt_header_input_obj_b" not in headers + + # Create a new Test from the headers and check its properties + next_synapse = synapse.from_headers(synapse.to_headers()) + assert next_synapse.a == 0 # Default value is 0 + assert next_synapse.b is None + assert next_synapse.c == 0 # Default is 0 + assert next_synapse.d == [] # Default is [] + assert next_synapse.e == [] # Empty list is default for list types + assert next_synapse.f is None + assert next_synapse.g is None + + +def test_body_hash_override(): + # Create a Synapse instance + synapse_instance = Synapse() + + # Try to set the body_hash property and expect an AttributeError + with pytest.raises( + AttributeError, + match="body_hash property is read-only and cannot be overridden.", + ): + synapse_instance.body_hash = [] + + +def test_default_instance_fields_dict_consistency(): + synapse_instance = Synapse() + assert synapse_instance.model_dump() == { + "name": "Synapse", + "timeout": 12.0, + "total_size": 0, + "header_size": 0, + "dendrite": { + "status_code": None, + "status_message": None, + "process_time": None, + "ip": None, + "port": None, + "version": None, + "nonce": None, + "uuid": None, + "hotkey": None, + "signature": None, + }, + "axon": { + "status_code": None, + "status_message": None, + "process_time": None, + "ip": None, + "port": None, + "version": None, + "nonce": None, + "uuid": None, + "hotkey": None, + "signature": None, + }, + "computed_body_hash": "", + } + + +class LegacyHashedSynapse(Synapse): + """Legacy Synapse subclass that serialized `required_hash_fields`.""" + + a: int + b: int + c: Optional[int] = None + d: Optional[list[str]] = None + required_hash_fields: Optional[list[str]] = ["b", "a", "d"] + + +class HashedSynapse(Synapse): + a: int + b: int + c: Optional[int] = None + d: Optional[list[str]] = None + required_hash_fields: ClassVar[tuple[str, ...]] = ("a", "b", "d") + + +@pytest.mark.parametrize("synapse_cls", [LegacyHashedSynapse, HashedSynapse]) +def test_synapse_body_hash(synapse_cls): + synapse_instance = synapse_cls(a=1, b=2, d=["foobar"]) + assert ( + synapse_instance.body_hash + == "ae06397d08f30f75c91395c59f05c62ac3b62b88250eb78b109213258e6ced0c" + ) + + # Extra non-hashed values should not influence the body hash + synapse_instance_slightly_different = synapse_cls(d=["foobar"], c=3, a=1, b=2) + assert synapse_instance.body_hash == synapse_instance_slightly_different.body_hash + + # Even if someone tries to override the required_hash_fields, it should still be the same + synapse_instance_try_override_hash_fields = synapse_cls( + a=1, b=2, d=["foobar"], required_hash_fields=["a"] + ) + assert ( + synapse_instance.body_hash + == synapse_instance_try_override_hash_fields.body_hash + ) + + # Different hashed values should result in different body hashes + synapse_different = synapse_cls(a=1, b=2) + assert synapse_instance.body_hash != synapse_different.body_hash diff --git a/tests/unit_tests/test_tensor.py b/tests/unit_tests/test_tensor.py new file mode 100644 index 0000000000..5a1ada61cf --- /dev/null +++ b/tests/unit_tests/test_tensor.py @@ -0,0 +1,228 @@ +import numpy +import numpy as np +import pytest +import torch + +from bittensor.core.tensor import Tensor + + +# This is a fixture that creates an example tensor for testing +@pytest.fixture +def example_tensor(): + # Create a tensor from a list using PyTorch + data = np.array([1, 2, 3, 4]) + + # Serialize the tensor into a Tensor instance and return it + return Tensor.serialize(data) + + +@pytest.fixture +def example_tensor_torch(force_legacy_torch_compatible_api): + # Create a tensor from a list using PyTorch + data = torch.tensor([1, 2, 3, 4]) + + # Serialize the tensor into a Tensor instance and return it + return Tensor.serialize(data) + + +def test_deserialize(example_tensor): + # Deserialize the tensor from the Tensor instance + tensor = example_tensor.deserialize() + + # Check that the result is a np.array with the correct values + assert isinstance(tensor, np.ndarray) + assert tensor.tolist() == [1, 2, 3, 4] + + +def test_deserialize_torch(example_tensor_torch, force_legacy_torch_compatible_api): + tensor = example_tensor_torch.deserialize() + # Check that the result is a PyTorch tensor with the correct values + assert isinstance(tensor, torch.Tensor) + assert tensor.tolist() == [1, 2, 3, 4] + + +def test_serialize(example_tensor): + # Check that the serialized tensor is an instance of Tensor + assert isinstance(example_tensor, Tensor) + + # Check that the Tensor instance has the correct buffer, dtype, and shape + assert example_tensor.buffer == example_tensor.buffer + assert example_tensor.dtype == example_tensor.dtype + assert example_tensor.shape == example_tensor.shape + + assert isinstance(example_tensor.tolist(), list) + + # Check that the Tensor instance has the correct buffer, dtype, and shape + assert example_tensor.buffer == example_tensor.buffer + assert example_tensor.dtype == example_tensor.dtype + assert example_tensor.shape == example_tensor.shape + + assert isinstance(example_tensor.numpy(), numpy.ndarray) + + # Check that the Tensor instance has the correct buffer, dtype, and shape + assert example_tensor.buffer == example_tensor.buffer + assert example_tensor.dtype == example_tensor.dtype + assert example_tensor.shape == example_tensor.shape + + assert isinstance(example_tensor.tensor(), np.ndarray) + + # Check that the Tensor instance has the correct buffer, dtype, and shape + assert example_tensor.buffer == example_tensor.buffer + assert example_tensor.dtype == example_tensor.dtype + assert example_tensor.shape == example_tensor.shape + + +def test_serialize_torch(example_tensor_torch, force_legacy_torch_compatible_api): + # Check that the serialized tensor is an instance of Tensor + assert isinstance(example_tensor_torch, Tensor) + + # Check that the Tensor instance has the correct buffer, dtype, and shape + assert example_tensor_torch.buffer == example_tensor_torch.buffer + assert example_tensor_torch.dtype == example_tensor_torch.dtype + assert example_tensor_torch.shape == example_tensor_torch.shape + + assert isinstance(example_tensor_torch.tolist(), list) + + # Check that the Tensor instance has the correct buffer, dtype, and shape + assert example_tensor_torch.buffer == example_tensor_torch.buffer + assert example_tensor_torch.dtype == example_tensor_torch.dtype + assert example_tensor_torch.shape == example_tensor_torch.shape + + assert isinstance(example_tensor_torch.numpy(), numpy.ndarray) + + # Check that the Tensor instance has the correct buffer, dtype, and shape + assert example_tensor_torch.buffer == example_tensor_torch.buffer + assert example_tensor_torch.dtype == example_tensor_torch.dtype + assert example_tensor_torch.shape == example_tensor_torch.shape + + assert isinstance(example_tensor_torch.tensor(), torch.Tensor) + + # Check that the Tensor instance has the correct buffer, dtype, and shape + assert example_tensor_torch.buffer == example_tensor_torch.buffer + assert example_tensor_torch.dtype == example_tensor_torch.dtype + assert example_tensor_torch.shape == example_tensor_torch.shape + + +def test_buffer_field(): + # Create a Tensor instance with a specified buffer, dtype, and shape + tensor = Tensor( + buffer="0x321e13edqwds231231231232131", dtype="float32", shape=[3, 3] + ) + + # Check that the buffer field matches the provided value + assert tensor.buffer == "0x321e13edqwds231231231232131" + + +def test_buffer_field_torch(force_legacy_torch_compatible_api): + # Create a Tensor instance with a specified buffer, dtype, and shape + tensor = Tensor( + buffer="0x321e13edqwds231231231232131", dtype="torch.float32", shape=[3, 3] + ) + + # Check that the buffer field matches the provided value + assert tensor.buffer == "0x321e13edqwds231231231232131" + + +def test_dtype_field(): + # Create a Tensor instance with a specified buffer, dtype, and shape + tensor = Tensor( + buffer="0x321e13edqwds231231231232131", dtype="float32", shape=[3, 3] + ) + + # Check that the dtype field matches the provided value + assert tensor.dtype == "float32" + + +def test_dtype_field_torch(force_legacy_torch_compatible_api): + tensor = Tensor( + buffer="0x321e13edqwds231231231232131", dtype="torch.float32", shape=[3, 3] + ) + assert tensor.dtype == "torch.float32" + + +def test_shape_field(): + # Create a Tensor instance with a specified buffer, dtype, and shape + tensor = Tensor( + buffer="0x321e13edqwds231231231232131", dtype="float32", shape=[3, 3] + ) + + # Check that the shape field matches the provided value + assert tensor.shape == [3, 3] + + +def test_shape_field_torch(force_legacy_torch_compatible_api): + tensor = Tensor( + buffer="0x321e13edqwds231231231232131", dtype="torch.float32", shape=[3, 3] + ) + assert tensor.shape == [3, 3] + + +def test_serialize_all_types(): + Tensor.serialize(np.array([1], dtype=np.float16)) + Tensor.serialize(np.array([1], dtype=np.float32)) + Tensor.serialize(np.array([1], dtype=np.float64)) + Tensor.serialize(np.array([1], dtype=np.uint8)) + Tensor.serialize(np.array([1], dtype=np.int32)) + Tensor.serialize(np.array([1], dtype=np.int64)) + Tensor.serialize(np.array([1], dtype=bool)) + + +def test_serialize_all_types_torch(force_legacy_torch_compatible_api): + Tensor.serialize(torch.tensor([1], dtype=torch.float16)) + Tensor.serialize(torch.tensor([1], dtype=torch.float32)) + Tensor.serialize(torch.tensor([1], dtype=torch.float64)) + Tensor.serialize(torch.tensor([1], dtype=torch.uint8)) + Tensor.serialize(torch.tensor([1], dtype=torch.int32)) + Tensor.serialize(torch.tensor([1], dtype=torch.int64)) + Tensor.serialize(torch.tensor([1], dtype=torch.bool)) + + +def test_serialize_all_types_equality(): + rng = np.random.default_rng() + + tensor = rng.standard_normal((100,), dtype=np.float32) + assert np.all(Tensor.serialize(tensor).tensor() == tensor) + + tensor = rng.standard_normal((100,), dtype=np.float64) + assert np.all(Tensor.serialize(tensor).tensor() == tensor) + + tensor = np.random.randint(255, 256, (1000,), dtype=np.uint8) + assert np.all(Tensor.serialize(tensor).tensor() == tensor) + + tensor = np.random.randint(2_147_483_646, 2_147_483_647, (1000,), dtype=np.int32) + assert np.all(Tensor.serialize(tensor).tensor() == tensor) + + tensor = np.random.randint( + 9_223_372_036_854_775_806, 9_223_372_036_854_775_807, (1000,), dtype=np.int64 + ) + assert np.all(Tensor.serialize(tensor).tensor() == tensor) + + tensor = rng.standard_normal((100,), dtype=np.float32) < 0.5 + assert np.all(Tensor.serialize(tensor).tensor() == tensor) + + +def test_serialize_all_types_equality_torch(force_legacy_torch_compatible_api): + torchtensor = torch.randn([100], dtype=torch.float16) + assert torch.all(Tensor.serialize(torchtensor).tensor() == torchtensor) + + torchtensor = torch.randn([100], dtype=torch.float32) + assert torch.all(Tensor.serialize(torchtensor).tensor() == torchtensor) + + torchtensor = torch.randn([100], dtype=torch.float64) + assert torch.all(Tensor.serialize(torchtensor).tensor() == torchtensor) + + torchtensor = torch.randint(255, 256, (1000,), dtype=torch.uint8) + assert torch.all(Tensor.serialize(torchtensor).tensor() == torchtensor) + + torchtensor = torch.randint( + 2_147_483_646, 2_147_483_647, (1000,), dtype=torch.int32 + ) + assert torch.all(Tensor.serialize(torchtensor).tensor() == torchtensor) + + torchtensor = torch.randint( + 9_223_372_036_854_775_806, 9_223_372_036_854_775_807, (1000,), dtype=torch.int64 + ) + assert torch.all(Tensor.serialize(torchtensor).tensor() == torchtensor) + + torchtensor = torch.randn([100], dtype=torch.float32) < 0.5 + assert torch.all(Tensor.serialize(torchtensor).tensor() == torchtensor) diff --git a/tests/unit_tests/utils/__init__.py b/tests/unit_tests/utils/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/unit_tests/utils/test_balance.py b/tests/unit_tests/utils/test_balance.py new file mode 100644 index 0000000000..4ff97bdb81 --- /dev/null +++ b/tests/unit_tests/utils/test_balance.py @@ -0,0 +1,520 @@ +"""Test the Balance class.""" + +from typing import Union + +import pytest +from hypothesis import given +from hypothesis import strategies as st + +from bittensor.utils.balance import Balance +from tests.helpers import CloseInValue + + +valid_tao_numbers_strategy = st.one_of( + st.integers(max_value=21_000_000, min_value=-21_000_000), + st.floats( + allow_infinity=False, + allow_nan=False, + allow_subnormal=False, + max_value=21_000_000.00, + min_value=-21_000_000.00, + ), +) + + +def remove_zero_filter(x): + """Remove zero and rounded to zero from the list of valid numbers""" + return int(x * pow(10, 9)) != 0 + + +@given(balance=valid_tao_numbers_strategy) +def test_balance_init(balance: Union[int, float]): + """ + Test the initialization of the Balance object. + """ + balance_ = Balance(balance) + if isinstance(balance, int): + assert balance_.rao == balance + elif isinstance(balance, float): + assert balance_.tao == CloseInValue(balance, 0.00001) + + +@given(balance=valid_tao_numbers_strategy, balance2=valid_tao_numbers_strategy) +def test_balance_add(balance: Union[int, float], balance2: Union[int, float]): + """ + Test the addition of two Balance objects. + """ + balance_ = Balance(balance) + balance2_ = Balance(balance2) + rao_: int + rao2_: int + if isinstance(balance, int): + rao_ = balance + elif isinstance(balance, float): + rao_ = int(balance * pow(10, 9)) + if isinstance(balance2, int): + rao2_ = balance2 + elif isinstance(balance2, float): + rao2_ = int(balance2 * pow(10, 9)) + + sum_ = balance_ + balance2_ + assert isinstance(sum_, Balance) + assert CloseInValue(sum_.rao, 5) == rao_ + rao2_ + + +@given(balance=valid_tao_numbers_strategy, balance2=valid_tao_numbers_strategy) +def test_balance_add_other_not_balance( + balance: Union[int, float], balance2: Union[int, float] +): + """ + Test the addition of a Balance object and a non-Balance object. + """ + balance_ = Balance(balance) + balance2_ = balance2 + rao_: int + rao2_: int + if isinstance(balance, int): + rao_ = balance + elif isinstance(balance, float): + rao_ = int(balance * pow(10, 9)) + # convert balance2 to rao. Assume balance2 was rao + rao2_ = int(balance2) + + sum_ = balance_ + balance2_ + assert isinstance(sum_, Balance) + assert CloseInValue(sum_.rao, 5) == rao_ + rao2_ + + +@given(balance=valid_tao_numbers_strategy) +def test_balance_eq_other_not_balance(balance: Union[int, float]): + """ + Test the equality of a Balance object and a non-Balance object. + """ + balance_ = Balance(balance) + rao2_: int + # convert balance2 to rao. This assumes balance2 is a rao value + rao2_ = int(balance_.rao) + + assert CloseInValue(rao2_, 5) == balance_ + + +@given(balance=valid_tao_numbers_strategy, balance2=valid_tao_numbers_strategy) +def test_balance_radd_other_not_balance( + balance: Union[int, float], balance2: Union[int, float] +): + """ + Test the right addition (radd) of a Balance object and a non-Balance object. + """ + balance_ = Balance(balance) + balance2_ = balance2 + rao_: int + rao2_: int + if isinstance(balance, int): + rao_ = balance + elif isinstance(balance, float): + rao_ = int(balance * pow(10, 9)) + # assume balance2 is a rao value + rao2_ = int(balance2) + + sum_ = balance2_ + balance_ # This is an radd + assert isinstance(sum_, Balance) + assert CloseInValue(sum_.rao, 5) == rao2_ + rao_ + + +@given(balance=valid_tao_numbers_strategy, balance2=valid_tao_numbers_strategy) +def test_balance_sub(balance: Union[int, float], balance2: Union[int, float]): + """ + Test the subtraction of two Balance objects. + """ + balance_ = Balance(balance) + balance2_ = Balance(balance2) + rao_: int + rao2_: int + if isinstance(balance, int): + rao_ = balance + elif isinstance(balance, float): + rao_ = int(balance * pow(10, 9)) + if isinstance(balance2, int): + rao2_ = balance2 + elif isinstance(balance2, float): + rao2_ = int(balance2 * pow(10, 9)) + + diff_ = balance_ - balance2_ + assert isinstance(diff_, Balance) + assert CloseInValue(diff_.rao, 5) == rao_ - rao2_ + + +@given(balance=valid_tao_numbers_strategy, balance2=valid_tao_numbers_strategy) +def test_balance_sub_other_not_balance( + balance: Union[int, float], balance2: Union[int, float] +): + """ + Test the subtraction of a Balance object and a non-Balance object. + """ + balance_ = Balance(balance) + balance2_ = balance2 + rao_: int + rao2_: int + if isinstance(balance, int): + rao_ = balance + elif isinstance(balance, float): + rao_ = int(balance * pow(10, 9)) + # assume balance2 is a rao value + rao2_ = int(balance2) + + diff_ = balance_ - balance2_ + assert isinstance(diff_, Balance) + assert CloseInValue(diff_.rao, 5) == rao_ - rao2_ + + +@given(balance=valid_tao_numbers_strategy, balance2=valid_tao_numbers_strategy) +def test_balance_rsub_other_not_balance( + balance: Union[int, float], balance2: Union[int, float] +): + """ + Test the right subtraction (rsub) of a Balance object and a non-Balance object. + """ + balance_ = Balance(balance) + balance2_ = balance2 + rao_: int + rao2_: int + if isinstance(balance, int): + rao_ = balance + elif isinstance(balance, float): + rao_ = int(balance * pow(10, 9)) + # assume balance2 is a rao value + rao2_ = int(balance2) + + diff_ = balance2_ - balance_ # This is an rsub + assert isinstance(diff_, Balance) + assert CloseInValue(diff_.rao, 5) == rao2_ - rao_ + + +@given(balance=valid_tao_numbers_strategy, balance2=valid_tao_numbers_strategy) +def test_balance_mul(balance: Union[int, float], balance2: Union[int, float]): + """ + Test the multiplication of two Balance objects. + """ + balance_ = Balance(balance) + balance2_ = Balance(balance2) + rao_: int + if isinstance(balance, int): + rao_ = balance + elif isinstance(balance, float): + rao_ = int(balance * pow(10, 9)) + if isinstance(balance2, int): + rao2_ = balance2 + elif isinstance(balance2, float): + rao2_ = int(balance2 * pow(10, 9)) + + prod_ = balance_ * balance2_ + assert isinstance(prod_, Balance) + + assert prod_.rao == pytest.approx(rao_ * rao2_, 9), ( + f"{balance_} * {balance2_} == {prod_.rao} != {rao_} * {balance2} == {rao_ * balance2}" + ) + + +@given(balance=valid_tao_numbers_strategy, balance2=valid_tao_numbers_strategy) +def test_balance_mul_other_not_balance( + balance: Union[int, float], balance2: Union[int, float] +): + """ + Test the multiplication of a Balance object and a non-Balance object. + """ + balance_ = Balance(balance) + balance2_ = balance2 + rao_: int + if isinstance(balance, int): + rao_ = balance + elif isinstance(balance, float): + rao_ = int(balance * pow(10, 9)) + + prod_ = balance_ * balance2_ + assert isinstance(prod_, Balance) + + assert abs(prod_.rao - int(rao_ * balance2)) <= 20, ( + f"{prod_.rao} != {int(rao_ * balance2)}" + ) + assert prod_.rao == pytest.approx(int(rao_ * balance2)) + + +@given(balance=valid_tao_numbers_strategy, balance2=valid_tao_numbers_strategy) +def test_balance_rmul_other_not_balance( + balance: Union[int, float], balance2: Union[int, float] +): + """ + Test the right multiplication (rmul) of a Balance object and a non-Balance object. + """ + balance_ = Balance(balance) + balance2_ = balance2 + rao_: int + if isinstance(balance, int): + rao_ = balance + elif isinstance(balance, float): + rao_ = int(balance * pow(10, 9)) + + prod_ = balance2_ * balance_ # This is an rmul + assert isinstance(prod_, Balance) + + assert abs(prod_.rao - int(balance2 * rao_)) <= 20, ( + f"{prod_.rao} != {int(balance2 * rao_)}" + ) + assert prod_.rao == pytest.approx(int(balance2 * rao_)) + + +@given( + balance=valid_tao_numbers_strategy, + balance2=valid_tao_numbers_strategy.filter(remove_zero_filter), +) # Avoid zero division +def test_balance_truediv(balance: Union[int, float], balance2: Union[int, float]): + """ + Test the true division (/) of two Balance objects. + """ + balance_ = Balance(balance) + balance2_ = Balance(balance2) + rao_: int + rao2_: int + if isinstance(balance, int): + rao_ = balance + elif isinstance(balance, float): + rao_ = int(balance * pow(10, 9)) + if isinstance(balance2, int): + rao2_ = balance2 + elif isinstance(balance2, float): + rao2_ = int(balance2 * pow(10, 9)) + + quot_ = balance_ / balance2_ + assert isinstance(quot_, Balance) + assert abs(quot_.rao - int(rao_ / rao2_)) <= 2, ( + f"{quot_.rao} != {int(rao_ / rao2_)}" + ) + assert quot_.rao == pytest.approx(int(rao_ / rao2_)) + + +@given( + balance=valid_tao_numbers_strategy, + balance2=valid_tao_numbers_strategy.filter(remove_zero_filter), +) +def test_balance_truediv_other_not_balance( + balance: Union[int, float], balance2: Union[int, float] +): + """ + Test the true division (/) of a Balance object and a non-Balance object. + """ + balance_ = Balance(balance) + balance2_ = balance2 + rao_: int + rao2_: int + if isinstance(balance, int): + rao_ = balance + elif isinstance(balance, float): + rao_ = int(balance * pow(10, 9)) + # assume balance2 is a rao value + rao2_ = balance2 + + quot_ = balance_ / balance2_ + assert quot_.rao == pytest.approx(int(rao_ / rao2_)) + assert abs(quot_.rao - int(rao_ / rao2_)) <= 10, ( + f"{quot_.rao} != {int(rao_ / rao2_)}" + ) + + +@given( + balance=valid_tao_numbers_strategy.filter(remove_zero_filter), + balance2=valid_tao_numbers_strategy, +) # This is a filter to avoid division by zero +def test_balance_rtruediv_other_not_balance( + balance: Union[int, float], balance2: Union[int, float] +): + """ + Test the right true division (rtruediv) of a Balance object and a non-Balance object. + """ + balance_ = Balance(balance) + balance2_ = balance2 + rao_: int + rao2_: int + if isinstance(balance, int): + rao_ = balance + elif isinstance(balance, float): + rao_ = int(balance * pow(10, 9)) + # assume balance2 is a rao value + rao2_ = balance2 + + quot_ = balance2_ / balance_ # This is an rtruediv + assert isinstance(quot_, Balance) + expected_value = int(rao2_ / rao_) + assert abs(quot_.rao - expected_value) <= 5, ( + f"{balance2_} / {balance_} = {quot_.rao} != {expected_value}" + ) + assert quot_.rao == pytest.approx(expected_value) + + +@given( + balance=valid_tao_numbers_strategy, + balance2=valid_tao_numbers_strategy.filter(remove_zero_filter), +) # Avoid zero division +def test_balance_floordiv(balance: Union[int, float], balance2: Union[int, float]): + """ + Test the floor division (//) of two Balance objects. + """ + balance_ = Balance(balance) + balance2_ = Balance(balance2) + rao_: int + rao2_: int + if isinstance(balance, int): + rao_ = balance + elif isinstance(balance, float): + rao_ = int(balance * pow(10, 9)) + if isinstance(balance2, int): + rao2_ = balance2 + elif isinstance(balance2, float): + rao2_ = int(balance2 * pow(10, 9)) + + quot_ = balance_ // balance2_ + assert isinstance(quot_, Balance) + assert CloseInValue(quot_.rao, 5) == rao_ // rao2_ + + +@given( + balance=valid_tao_numbers_strategy, + balance2=valid_tao_numbers_strategy.filter(remove_zero_filter), +) +def test_balance_floordiv_other_not_balance( + balance: Union[int, float], balance2: Union[int, float] +): + """ + Test the floor division (//) of a Balance object and a non-Balance object. + """ + balance_ = Balance(balance) + balance2_ = balance2 + rao_: int + rao2_: int + if isinstance(balance, int): + rao_ = balance + elif isinstance(balance, float): + rao_ = int(balance * pow(10, 9)) + # assume balance2 is a rao value + rao2_ = balance2 + + quot_ = balance_ // balance2_ + assert isinstance(quot_, Balance) + expected_value = rao_ // rao2_ + assert abs(quot_.rao - expected_value) <= 5, ( + f"{balance_} // {balance2_} = {quot_.rao} != {expected_value}" + ) + assert quot_.rao == pytest.approx(rao_ // rao2_) + + +@given( + balance=valid_tao_numbers_strategy.filter(remove_zero_filter), + balance2=valid_tao_numbers_strategy, +) # This is a filter to avoid division by zero +def test_balance_rfloordiv_other_not_balance( + balance: Union[int, float], balance2: Union[int, float] +): + """ + Test the right floor division (rfloordiv) of a Balance object and a non-Balance object. + """ + balance_ = Balance(balance) + balance2_ = balance2 + rao_: int + rao2_: int + if isinstance(balance, int): + rao_ = balance + elif isinstance(balance, float): + rao_ = int(balance * pow(10, 9)) + # assume balance2 is a rao value + rao2_ = balance2 + + quot_ = balance2_ // balance_ # This is an rfloordiv + assert isinstance(quot_, Balance) + expected_value = rao2_ // rao_ + assert quot_.rao == pytest.approx(rao2_ // rao_) + assert abs(quot_.rao - expected_value) <= 5 + + +@given(balance=valid_tao_numbers_strategy) +def test_balance_not_eq_none(balance: Union[int, float]): + """ + Test the inequality (!=) of a Balance object and None. + """ + balance_ = Balance(balance) + assert balance_ is not None + + +@given(balance=valid_tao_numbers_strategy) +def test_balance_neq_none(balance: Union[int, float]): + """ + Test the inequality (!=) of a Balance object and None. + """ + balance_ = Balance(balance) + assert balance_ is not None + + +def test_balance_init_from_invalid_value(): + """ + Test the initialization of a Balance object with an invalid value. + """ + with pytest.raises(TypeError): + Balance("invalid not a number") + + +@given(balance=valid_tao_numbers_strategy) +def test_balance_add_invalid_type(balance: Union[int, float]): + """ + Test the addition of a Balance object with an invalid type. + """ + balance_ = Balance(balance) + with pytest.raises(NotImplementedError): + _ = balance_ + "" + + +@given(balance=valid_tao_numbers_strategy) +def test_balance_sub_invalid_type(balance: Union[int, float]): + """ + Test the subtraction of a Balance object with an invalid type. + """ + balance_ = Balance(balance) + with pytest.raises(NotImplementedError): + _ = balance_ - "" + + +@given(balance=valid_tao_numbers_strategy) +def test_balance_div_invalid_type(balance: Union[int, float]): + """ + Test the division of a Balance object with an invalid type. + """ + balance_ = Balance(balance) + with pytest.raises(NotImplementedError): + _ = balance_ / "" + + +@given(balance=valid_tao_numbers_strategy) +def test_balance_mul_invalid_type(balance: Union[int, float]): + """ + Test the multiplication of a Balance object with an invalid type. + """ + balance_ = Balance(balance) + with pytest.raises(NotImplementedError): + _ = balance_ * "" + + +@given(balance=valid_tao_numbers_strategy) +def test_balance_eq_invalid_type(balance: Union[int, float]): + """ + Test the equality of a Balance object with an invalid type. + """ + balance_ = Balance(balance) + with pytest.raises(NotImplementedError): + balance_ == "" + + +def test_from_float(): + """Tests from_float method call.""" + assert Balance.from_tao(1.0) == Balance(1000000000) + + +def test_from_rao(): + """Tests from_rao method call.""" + assert Balance.from_tao(1) == Balance(1000000000) diff --git a/tests/unit_tests/utils/test_formatting.py b/tests/unit_tests/utils/test_formatting.py new file mode 100644 index 0000000000..57ba6541d3 --- /dev/null +++ b/tests/unit_tests/utils/test_formatting.py @@ -0,0 +1,63 @@ +import math + +from bittensor.utils import formatting + + +def test_get_human_readable(): + """Tests the `get_human_readable` function in the `formatting` module.""" + num1 = 1000 + num2 = 1_000_000 + num3 = 1_000_000_000 + num4 = 150 + negative_num = -1000 + + # Test for default output + assert formatting.get_human_readable(num1) == "1.0KH" + + # Test for different quantities + assert formatting.get_human_readable(num2) == "1.0MH" + assert formatting.get_human_readable(num3) == "1.0GH" + + # Test for numbers less than 1000 + assert formatting.get_human_readable(num4) == "150.0H" + + # Test for negative numbers + assert formatting.get_human_readable(negative_num) == "-1.0KH" + + # Test for different suffix + assert formatting.get_human_readable(num1, suffix="B") == "1.0KB" + assert formatting.get_human_readable(num2, suffix="B") == "1.0MB" + assert formatting.get_human_readable(num3, suffix="B") == "1.0GB" + assert formatting.get_human_readable(num4, suffix="B") == "150.0B" + assert formatting.get_human_readable(negative_num, suffix="B") == "-1.0KB" + + +def test_millify(): + """Test millify function with various cases.""" + # Testing with value 0 + assert formatting.millify(0) == "0.00" + # Testing with a number in the tens + assert formatting.millify(10) == "10.00" + # Testing with a number in the hundreds + assert formatting.millify(100) == "100.00" + # Testing with a number in the thousands + assert formatting.millify(1000) == "1.00 K" + # Testing with a number in the millions + assert formatting.millify(1000000) == "1.00 M" + # Testing with a number in the billions + assert formatting.millify(1000000000) == "1.00 B" + # Testing with a number in the trillions + assert formatting.millify(1000000000000) == "1.00 T" + # Testing with maximum limit + mill_names = ["", " K", " M", " B", " T"] + n = 10 ** (3 * (len(mill_names) - 1) + 1) + mill_idx = max( + 0, + min( + len(mill_names) - 1, + int(math.floor(0 if n == 0 else math.log10(abs(n)) / 3)), + ), + ) + assert formatting.millify(n) == "{:.2f}{}".format( + n / 10 ** (3 * mill_idx), mill_names[mill_idx] + ) diff --git a/tests/unit_tests/utils/test_liquidity_utils.py b/tests/unit_tests/utils/test_liquidity_utils.py new file mode 100644 index 0000000000..5a761cc6a7 --- /dev/null +++ b/tests/unit_tests/utils/test_liquidity_utils.py @@ -0,0 +1,124 @@ +import math + +import pytest + +from bittensor.utils.balance import Balance +from bittensor.utils.liquidity import ( + LiquidityPosition, + price_to_tick, + tick_to_price, + get_fees, + get_fees_in_range, + calculate_fees, +) + + +def test_liquidity_position_to_token_amounts(): + """Test conversion of liquidity position to token amounts.""" + # Preps + pos = LiquidityPosition( + id=1, + price_low=Balance.from_tao(10000), + price_high=Balance.from_tao(40000), + liquidity=Balance.from_tao(25000), + fees_tao=Balance.from_tao(0), + fees_alpha=Balance.from_tao(0), + netuid=1, + ) + current_price = Balance.from_tao(20000) + # Call + alpha, tao = pos.to_token_amounts(current_price) + # Asserts + assert isinstance(alpha, Balance) + assert isinstance(tao, Balance) + assert alpha.rao >= 0 and tao.rao >= 0 + + +def test_price_to_tick_and_back(): + """Test price to tick conversion and back.""" + # Preps + price = 1.25 + # Call + tick = price_to_tick(price) + restored_price = tick_to_price(tick) + # Asserts + assert math.isclose(restored_price, price, rel_tol=1e-3) + + +def test_price_to_tick_invalid(): + """Test price to tick conversion with invalid input.""" + with pytest.raises(ValueError): + price_to_tick(0) + + +def test_tick_to_price_invalid(): + """Test tick to price conversion with invalid input.""" + with pytest.raises(ValueError): + tick_to_price(1_000_000) + + +def test_get_fees_above_true(): + """Test fee calculation for above position.""" + # Preps + tick = { + "liquidity_net": 1000000000000, + "liquidity_gross": 1000000000000, + "fees_out_tao": {"bits": 0}, + "fees_out_alpha": {"bits": 0}, + } + # Call + result = get_fees( + current_tick=100, + tick=tick, + tick_index=90, + quote=True, + global_fees_tao=8000, + global_fees_alpha=6000, + above=True, + ) + # Asserts + assert result == 8000 + + +def test_get_fees_in_range(): + """Test fee calculation within a range.""" + # Call + value = get_fees_in_range( + quote=True, + global_fees_tao=10000, + global_fees_alpha=5000, + fees_below_low=2000, + fees_above_high=1000, + ) + # Asserts + assert value == 7000 + + +def test_calculate_fees(): + """Test calculation of fees for a position.""" + # Preps + position = { + "id": (2,), + "netuid": 2, + "tick_low": (206189,), + "tick_high": (208196,), + "liquidity": 1000000000000, + "fees_tao": {"bits": 0}, + "fees_alpha": {"bits": 0}, + } + # Call + result = calculate_fees( + position=position, + global_fees_tao=5000, + global_fees_alpha=8000, + tao_fees_below_low=1000, + tao_fees_above_high=1000, + alpha_fees_below_low=2000, + alpha_fees_above_high=1000, + netuid=1, + ) + # Asserts + assert isinstance(result[0], Balance) + assert isinstance(result[1], Balance) + assert result[0].rao > 0 + assert result[1].rao > 0 diff --git a/tests/unit_tests/utils/test_networking.py b/tests/unit_tests/utils/test_networking.py new file mode 100644 index 0000000000..a3f2c54ac6 --- /dev/null +++ b/tests/unit_tests/utils/test_networking.py @@ -0,0 +1,198 @@ +import os +import urllib +import pytest +import requests +import unittest.mock as mock +from bittensor import utils +from unittest.mock import MagicMock + + +# Test conversion functions for IPv4 +def test_int_to_ip_zero(): + """Test converting integer to IPv4 address for 0.""" + assert utils.networking.int_to_ip(0) == "0.0.0.0" + assert utils.networking.ip_to_int("0.0.0.0") == 0 + assert utils.networking.ip__str__(4, "0.0.0.0", 8888) == "/ipv4/0.0.0.0:8888" + + +def test_int_to_ip_range(): + """Test converting integer to IPv4 addresses in a range.""" + for i in range(10): + assert utils.networking.int_to_ip(i) == f"0.0.0.{i}" + assert utils.networking.ip_to_int(f"0.0.0.{i}") == i + assert ( + utils.networking.ip__str__(4, f"0.0.0.{i}", 8888) == f"/ipv4/0.0.0.{i}:8888" + ) + + +def test_int_to_ip4_max(): + """Test converting integer to maximum IPv4 address.""" + assert utils.networking.int_to_ip(4294967295) == "255.255.255.255" + assert utils.networking.ip_to_int("255.255.255.255") == 4294967295 + assert ( + utils.networking.ip__str__(4, "255.255.255.255", 8888) + == "/ipv4/255.255.255.255:8888" + ) + + +# Test conversion functions for IPv6 +def test_int_to_ip6_zero(): + """Test converting integer to IPv6 address for 0.""" + assert utils.networking.int_to_ip(4294967296) == "::1:0:0" + assert utils.networking.ip_to_int("::1:0:0") == 4294967296 + assert utils.networking.ip__str__(6, "::1:0:0", 8888) == "/ipv6/::1:0:0:8888" + + +def test_int_to_ip6_range(): + """Test converting integer to IPv6 addresses in a range.""" + for i in range(10): + assert utils.networking.int_to_ip(4294967296 + i) == f"::1:0:{i}" + assert utils.networking.ip_to_int(f"::1:0:{i}") == 4294967296 + i + assert ( + utils.networking.ip__str__(6, f"::1:0:{i}", 8888) == f"/ipv6/::1:0:{i}:8888" + ) + + +def test_int_to_ip6_max(): + """Test converting integer to maximum IPv6 address.""" + max_val = 340282366920938463463374607431768211455 + assert ( + utils.networking.int_to_ip(max_val) == "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff" + ) + assert ( + utils.networking.ip_to_int("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff") == max_val + ) + assert ( + utils.networking.ip__str__(6, "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", 8888) + == "/ipv6/ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff:8888" + ) + + +def test_int_to_ip6_overflow(): + """Test handling overflow when converting integer to IPv6 address.""" + overflow = 340282366920938463463374607431768211455 + 1 + with pytest.raises(Exception): + utils.networking.int_to_ip(overflow) + + +def test_int_to_ip6_underflow(): + """Test handling underflow when converting integer to IPv6 address.""" + underflow = -1 + with pytest.raises(Exception): + utils.networking.int_to_ip(underflow) + + +# Test getting external IP address +def test_get_external_ip(mocker): + """Test getting the external IP address.""" + mocked_requests_get = mock.Mock( + return_value=mock.Mock( + **{ + "text": "192.168.1.1", + }, + ), + ) + + mocker.patch.object( + requests, + "get", + mocked_requests_get, + ) + + assert utils.networking.get_external_ip() == "192.168.1.1" + + mocked_requests_get.assert_called_once_with("https://checkip.amazonaws.com") + + +def test_get_external_ip_os_broken(mocker): + """Test getting the external IP address when os.popen is broken.""" + mocked_requests_get = mock.Mock( + return_value=mock.Mock( + **{ + "text": "192.168.1.1", + }, + ), + ) + + mocker.patch.object( + requests, + "get", + mocked_requests_get, + ) + + class FakeReadline: + def readline(self): + return 1 + + def mock_call(): + return FakeReadline() + + with mock.patch.object(os, "popen", new=mock_call): + assert utils.networking.get_external_ip() == "192.168.1.1" + + mocked_requests_get.assert_called_once_with("https://checkip.amazonaws.com") + + +def test_get_external_ip_os_request_urllib_broken(): + """Test getting the external IP address when os.popen and requests.get/urllib.request are broken.""" + + class FakeReadline: + def readline(self): + return 1 + + def mock_call(): + return FakeReadline() + + class FakeResponse: + def text(self): + return 1 + + def mock_call_two(): + return FakeResponse() + + class FakeRequest: + def urlopen(self): + return 1 + + with mock.patch.object(os, "popen", new=mock_call): + with mock.patch.object(requests, "get", new=mock_call_two): + urllib.request = MagicMock(return_value=FakeRequest()) + with pytest.raises(Exception): + assert utils.networking.get_external_ip() + + +# Test formatting WebSocket endpoint URL +@pytest.mark.parametrize( + "url, expected", + [ + ("wss://exampleendpoint:9944", "wss://exampleendpoint:9944"), + ("ws://exampleendpoint:9944", "ws://exampleendpoint:9944"), + ( + "exampleendpoint:9944", + "ws://exampleendpoint:9944", + ), # should add ws:// not wss:// + ( + "ws://exampleendpoint", + "ws://exampleendpoint", + ), # should not add port if not specified + ( + "wss://exampleendpoint", + "wss://exampleendpoint", + ), # should not add port if not specified + ( + "exampleendpoint", + "ws://exampleendpoint", + ), # should not add port if not specified + ( + "exampleendpointwithws://:9944", + "ws://exampleendpointwithws://:9944", + ), # should only care about the front + ( + "exampleendpointwithwss://:9944", + "ws://exampleendpointwithwss://:9944", + ), # should only care about the front + ], +) +def test_format(url: str, expected: str): + """Test formatting WebSocket endpoint URL.""" + assert utils.networking.get_formatted_ws_endpoint_url(url) == expected diff --git a/tests/unit_tests/utils/test_registration.py b/tests/unit_tests/utils/test_registration.py new file mode 100644 index 0000000000..a4ec066279 --- /dev/null +++ b/tests/unit_tests/utils/test_registration.py @@ -0,0 +1,47 @@ +import pytest + +from bittensor.utils.registration import LazyLoadedTorch + + +class MockBittensorLogging: + def __init__(self): + self.messages = [] + + def error(self, message): + self.messages.append(message) + + +@pytest.fixture +def mock_bittensor_logging(monkeypatch): + mock_logger = MockBittensorLogging() + monkeypatch.setattr("bittensor.utils.registration.pow.logging", mock_logger) + return mock_logger + + +def test_lazy_loaded_torch__torch_installed(monkeypatch, mock_bittensor_logging): + import torch + + lazy_torch = LazyLoadedTorch() + + assert bool(torch) is True + + assert lazy_torch.nn is torch.nn + with pytest.raises(AttributeError): + lazy_torch.no_such_thing + + +def test_lazy_loaded_torch__no_torch(monkeypatch, mock_bittensor_logging): + monkeypatch.setattr( + "bittensor.utils.registration.pow._get_real_torch", lambda: None + ) + + torch = LazyLoadedTorch() + + assert not torch + + with pytest.raises(ImportError): + torch.some_attribute + + # Check if the error message is logged correctly + assert len(mock_bittensor_logging.messages) == 1 + assert "This command requires torch." in mock_bittensor_logging.messages[0] diff --git a/tests/unit_tests/utils/test_utils.py b/tests/unit_tests/utils/test_utils.py new file mode 100644 index 0000000000..9a6527d643 --- /dev/null +++ b/tests/unit_tests/utils/test_utils.py @@ -0,0 +1,240 @@ +import pytest + +from bittensor import warnings, __getattr__, version_split, logging, trace, debug, utils +from bittensor.core.settings import SS58_FORMAT + + +def test_getattr_version_split(): + """Test that __getattr__ for 'version_split' issues a deprecation warning and returns the correct value.""" + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + assert __getattr__("version_split") == version_split + assert len(w) == 1 + assert issubclass(w[-1].category, DeprecationWarning) + assert "version_split is deprecated" in str(w[-1].message) + + +@pytest.mark.parametrize("test_input, expected", [(True, "Trace"), (False, "Default")]) +def test_trace(test_input, expected): + """Test the trace function turns tracing on|off.""" + trace(test_input) + assert logging.current_state_value == expected + + +@pytest.mark.parametrize("test_input, expected", [(True, "Debug"), (False, "Default")]) +def test_debug(test_input, expected): + """Test the debug function turns tracing on|off.""" + debug(test_input) + assert logging.current_state_value == expected + + +def test_ss58_to_vec_u8(mocker): + """Tests `utils.ss58_to_vec_u8` function.""" + # Prep + test_ss58_address = "5DD26kC2kxajmwfbbZmVmxhrY9VeeyR1Gpzy9i8wxLUg6zxm" + fake_return = b"2\xa6?" + mocked_ss58_address_to_bytes = mocker.patch.object( + utils, "ss58_address_to_bytes", return_value=fake_return + ) + + # Call + result = utils.ss58_to_vec_u8(test_ss58_address) + + # Asserts + mocked_ss58_address_to_bytes.assert_called_once_with(test_ss58_address) + assert result == [int(byte) for byte in fake_return] + + +@pytest.mark.parametrize( + "test_input,expected", + [ + ("y", True), + ("yes", True), + ("t", True), + ("true", True), + ("on", True), + ("1", True), + ("n", False), + ("no", False), + ("f", False), + ("false", False), + ("off", False), + ("0", False), + ], +) +def test_strtobool(test_input, expected): + """Test truthy values.""" + assert utils.strtobool(test_input) is expected + + +@pytest.mark.parametrize( + "test_input", + [ + "maybe", + "2", + "onoff", + ], +) +def test_strtobool_raise_error(test_input): + """Tests invalid values.""" + with pytest.raises(ValueError): + utils.strtobool(test_input) + + +def test_get_explorer_root_url_by_network_from_map(): + """Tests private utils._get_explorer_root_url_by_network_from_map function.""" + # Prep + # Test with a known network + network_map = { + "entity1": {"network1": "url1", "network2": "url2"}, + "entity2": {"network1": "url3", "network3": "url4"}, + } + # Test with no matching network in the map + network_map_empty = { + "entity1": {}, + "entity2": {}, + } + + # Assertions + assert utils._get_explorer_root_url_by_network_from_map( + "network1", network_map + ) == { + "entity1": "url1", + "entity2": "url3", + } + # Test with an unknown network + assert ( + utils._get_explorer_root_url_by_network_from_map("unknown_network", network_map) + == {} + ) + assert ( + utils._get_explorer_root_url_by_network_from_map("network1", network_map_empty) + == {} + ) + + +def test_get_explorer_url_for_network(): + """Tests `utils.get_explorer_url_for_network` function.""" + # Prep + fake_block_hash = "0x1234567890abcdef" + fake_map = {"opentensor": {"network": "url"}, "taostats": {"network": "url2"}} + + # Call + result = utils.get_explorer_url_for_network("network", fake_block_hash, fake_map) + + # Assert + assert result == { + "opentensor": f"url/query/{fake_block_hash}", + "taostats": f"url2/extrinsic/{fake_block_hash}", + } + + +def test_ss58_address_to_bytes(mocker): + """Tests utils.ss58_address_to_bytes function.""" + # Prep + fake_ss58_address = "ss58_address" + mocked_scalecodec_ss58_decode = mocker.patch.object( + utils.scalecodec, "ss58_decode", return_value="" + ) + + # Call + result = utils.ss58_address_to_bytes(fake_ss58_address) + + # Asserts + mocked_scalecodec_ss58_decode.assert_called_once_with( + fake_ss58_address, SS58_FORMAT + ) + assert result == bytes.fromhex(mocked_scalecodec_ss58_decode.return_value) + + +@pytest.mark.parametrize( + "test_input, expected_result", + [ + (123, False), + ("0x234SD", True), + ("5D34SD", True), + (b"0x234SD", True), + ], +) +def test_is_valid_bittensor_address_or_public_key(mocker, test_input, expected_result): + """Tests utils.is_valid_bittensor_address_or_public_key function.""" + # Prep + mocked_is_valid_ed25519_pubkey = mocker.patch.object( + utils, "_is_valid_ed25519_pubkey", return_value=True + ) + mocked_ss58_is_valid_ss58_address = mocker.patch.object( + utils, "_is_valid_ss58_address", side_effect=[False, True] + ) + + # Call + result = utils.is_valid_bittensor_address_or_public_key(test_input) + + # Asserts + if not isinstance(test_input, int) and isinstance(test_input, bytes): + mocked_is_valid_ed25519_pubkey.assert_called_with(test_input) + if isinstance(test_input, str) and not test_input.startswith("0x"): + assert mocked_ss58_is_valid_ss58_address.call_count == 2 + assert result == expected_result + + +@pytest.mark.parametrize( + "unlock_type, wallet_method", + [ + ("coldkey", "unlock_coldkey"), + ("hotkey", "unlock_hotkey"), + ], +) +def test_unlock_key(fake_wallet, unlock_type, wallet_method): + """Test the unlock key function.""" + + # Call + result = utils.unlock_key(fake_wallet, unlock_type=unlock_type) + + # Asserts + getattr(fake_wallet, wallet_method).assert_called_once() + assert result == utils.UnlockStatus(True, "") + + +def test_unlock_key_raise_value_error(fake_wallet): + """Test the unlock key function raises ValueError.""" + with pytest.raises(ValueError): + utils.unlock_key(wallet=fake_wallet, unlock_type="coldkeypub") + + +@pytest.mark.parametrize( + "side_effect, response", + [ + ( + utils.KeyFileError("Simulated KeyFileError exception"), + utils.UnlockStatus( + False, + "Coldkey keyfile is corrupt, non-writable, or non-readable, or non-existent.", + ), + ), + ( + utils.PasswordError("Simulated PasswordError exception"), + utils.UnlockStatus( + False, "The password used to decrypt your Coldkey keyfile is invalid." + ), + ), + ], + ids=["PasswordError", "KeyFileError"], +) +def test_unlock_key_errors(fake_wallet, side_effect, response): + """Test the unlock key function handles the errors.""" + fake_wallet.unlock_coldkey.side_effect = side_effect + result = utils.unlock_key(wallet=fake_wallet) + + assert result == response + + +@pytest.mark.parametrize( + "hex_str, response", + [ + ("5461796c6f72205377696674", b"Taylor Swift"), + ("0x5461796c6f72205377696674", b"Taylor Swift"), + ], +) +def test_hex_to_bytes(hex_str, response): + result = utils.hex_to_bytes(hex_str) + assert result == response diff --git a/tests/unit_tests/utils/test_version.py b/tests/unit_tests/utils/test_version.py new file mode 100644 index 0000000000..6d1785b0bd --- /dev/null +++ b/tests/unit_tests/utils/test_version.py @@ -0,0 +1,152 @@ +from pathlib import Path +import pytest +from freezegun import freeze_time +from datetime import datetime, timedelta, timezone + +# from bittensor.utils.version import ( +# VERSION_CHECK_THRESHOLD, +# VersionCheckError, +# get_and_save_latest_version, +# check_version, +# version_checking, +# __version__ +# ) +from bittensor.utils import version + +from unittest.mock import MagicMock +from pytest_mock import MockerFixture + + +@pytest.fixture +def pypi_version(): + return "6.9.3" + + +@pytest.fixture +def mock_get_version_from_pypi(mocker: MockerFixture, pypi_version: str): + return mocker.patch( + "bittensor.utils.version._get_version_from_pypi", + return_value=pypi_version, + autospec=True, + ) + + +@pytest.fixture +def version_file_path(mocker: MockerFixture, tmp_path: Path): + file_path = tmp_path / ".version" + + mocker.patch( + "bittensor.utils.version._get_version_file_path", return_value=file_path + ) + return file_path + + +def test_get_and_save_latest_version_no_file( + mock_get_version_from_pypi: MagicMock, version_file_path: Path, pypi_version: str +): + assert not version_file_path.exists() + + assert version.get_and_save_latest_version() == pypi_version + + mock_get_version_from_pypi.assert_called_once() + assert version_file_path.exists() + assert version_file_path.read_text() == pypi_version + + +@pytest.mark.parametrize("elapsed", [0, version.VERSION_CHECK_THRESHOLD - 5]) +def test_get_and_save_latest_version_file_fresh_check( + mock_get_version_from_pypi: MagicMock, version_file_path: Path, elapsed: int +): + now = datetime.now(timezone.utc) + + version_file_path.write_text("6.9.5") + + with freeze_time(now + timedelta(seconds=elapsed)): + assert version.get_and_save_latest_version() == "6.9.5" + + mock_get_version_from_pypi.assert_not_called() + + +def test_get_and_save_latest_version_file_expired_check( + mock_get_version_from_pypi: MagicMock, version_file_path: Path, pypi_version: str +): + now = datetime.now(timezone.utc) + + version_file_path.write_text("6.9.5") + + with freeze_time(now + timedelta(seconds=version.VERSION_CHECK_THRESHOLD + 1)): + assert version.get_and_save_latest_version() == pypi_version + + mock_get_version_from_pypi.assert_called_once() + assert version_file_path.read_text() == pypi_version + + +@pytest.mark.parametrize( + ("current_version", "latest_version"), + [ + ("6.9.3", "6.9.4"), + ("6.9.3a1", "6.9.3a2"), + ("6.9.3a1", "6.9.3b1"), + ("6.9.3", "6.10"), + ("6.9.3", "7.0"), + ("6.0.15", "6.1.0"), + ], +) +def test_check_version_newer_available( + mocker: MockerFixture, current_version: str, latest_version: str, capsys +): + version.__version__ = current_version + mocker.patch( + "bittensor.utils.version.get_and_save_latest_version", + return_value=latest_version, + ) + + version.check_version() + + captured = capsys.readouterr() + + assert "update" in captured.out + assert current_version in captured.out + assert latest_version in captured.out + + +@pytest.mark.parametrize( + ("current_version", "latest_version"), + [ + ("6.9.3", "6.9.3"), + ("6.9.3", "6.9.2"), + ("6.9.3b", "6.9.3a"), + ], +) +def test_check_version_up_to_date( + mocker: MockerFixture, current_version: str, latest_version: str, capsys +): + version.__version__ = current_version + mocker.patch( + "bittensor.utils.version.get_and_save_latest_version", + return_value=latest_version, + ) + + version.check_version() + + captured = capsys.readouterr() + + assert captured.out == "" + + +def test_version_checking(mocker: MockerFixture): + mock = mocker.patch("bittensor.utils.version.check_version") + + version.version_checking() + + mock.assert_called_once() + + +def test_version_checking_exception(mocker: MockerFixture): + mock = mocker.patch( + "bittensor.utils.version.check_version", side_effect=version.VersionCheckError + ) + + version.version_checking() + + mock.assert_called_once() diff --git a/tests/unit_tests/utils/test_weight_utils.py b/tests/unit_tests/utils/test_weight_utils.py new file mode 100644 index 0000000000..c378173bfa --- /dev/null +++ b/tests/unit_tests/utils/test_weight_utils.py @@ -0,0 +1,660 @@ +import logging +import numpy as np + +import bittensor.utils.weight_utils as weight_utils +import pytest + +from bittensor.utils import torch + + +def test_convert_weight_and_uids(): + uids = np.arange(10) + weights = np.random.rand(10) + weight_utils.convert_weights_and_uids_for_emit(uids, weights) + + # min weight < 0 + weights[5] = -1 + with pytest.raises(ValueError) as pytest_wrapped_e: + weight_utils.convert_weights_and_uids_for_emit(uids, weights) + + # min uid < 0 + weights[5] = 0 + uids[3] = -1 + with pytest.raises(ValueError) as pytest_wrapped_e: + weight_utils.convert_weights_and_uids_for_emit(uids, weights) + + # len(uids) != len(weights) + uids[3] = 3 + with pytest.raises(ValueError) as pytest_wrapped_e: + weight_utils.convert_weights_and_uids_for_emit(uids, weights[1:]) + + # sum(weights) == 0 + weights = np.zeros(10) + weight_utils.convert_weights_and_uids_for_emit(uids, weights) + + # test for overflow and underflow + for _ in range(5): + uids = np.arange(10) + weights = np.random.rand(10) + weight_utils.convert_weights_and_uids_for_emit(uids, weights) + + +def test_convert_weight_and_uids_torch(force_legacy_torch_compatible_api): + uids = torch.tensor(list(range(10))) + weights = torch.rand(10) + weight_utils.convert_weights_and_uids_for_emit(uids, weights) + + # min weight < 0 + weights[5] = -1 + with pytest.raises(ValueError) as pytest_wrapped_e: + weight_utils.convert_weights_and_uids_for_emit(uids, weights) + # min uid < 0 + weights[5] = 0 + uids[3] = -1 + with pytest.raises(ValueError) as pytest_wrapped_e: + weight_utils.convert_weights_and_uids_for_emit(uids, weights) + # len(uids) != len(weights) + uids[3] = 3 + with pytest.raises(ValueError) as pytest_wrapped_e: + weight_utils.convert_weights_and_uids_for_emit(uids, weights[1:]) + + # sum(weights) == 0 + weights = torch.zeros(10) + weight_utils.convert_weights_and_uids_for_emit(uids, weights) + + # test for overflow and underflow + for _ in range(5): + uids = torch.tensor(list(range(10))) + weights = torch.rand(10) + weight_utils.convert_weights_and_uids_for_emit(uids, weights) + + +def test_normalize_with_max_weight(): + weights = np.random.rand(1000) + wn = weight_utils.normalize_max_weight(weights, limit=0.01) + assert wn.max() <= 0.01 + + weights = np.zeros(1000) + wn = weight_utils.normalize_max_weight(weights, limit=0.01) + assert wn.max() <= 0.01 + + weights = np.random.rand(1000) + wn = weight_utils.normalize_max_weight(weights, limit=0.02) + assert wn.max() <= 0.02 + + weights = np.zeros(1000) + wn = weight_utils.normalize_max_weight(weights, limit=0.02) + assert wn.max() <= 0.02 + + weights = np.random.rand(1000) + wn = weight_utils.normalize_max_weight(weights, limit=0.03) + assert wn.max() <= 0.03 + + weights = np.zeros(1000) + wn = weight_utils.normalize_max_weight(weights, limit=0.03) + assert wn.max() <= 0.03 + + # Check for Limit + limit = 0.001 + weights = np.random.rand(2000) + w = weights / weights.sum() + wn = weight_utils.normalize_max_weight(weights, limit=limit) + assert abs((w.max() >= limit and (limit - wn.max())) < 0.001) or ( + w.max() < limit and wn.max() < limit + ) + + # Check for Zeros + limit = 0.01 + weights = np.zeros(2000) + wn = weight_utils.normalize_max_weight(weights, limit=limit) + assert wn.max() == 1 / 2000 + + # Check for Ordering after normalization + weights = np.random.rand(100) + wn = weight_utils.normalize_max_weight(weights, limit=1) + assert np.array_equal(wn, weights / weights.sum()) + + # Check for epsilon changes + epsilon = 0.01 + weights = np.sort(np.random.rand(100)) + x = weights / weights.sum() + limit = x[-10] + change = epsilon * limit + y = weight_utils.normalize_max_weight(x, limit=limit - change) + z = weight_utils.normalize_max_weight(x, limit=limit + change) + assert np.abs(y - z).sum() < epsilon + + +def test_normalize_with_max_weight__legacy_torch_api_compat( + force_legacy_torch_compatible_api, +): + weights = torch.rand(1000) + wn = weight_utils.normalize_max_weight(weights, limit=0.01) + assert wn.max() <= 0.01 + + weights = torch.zeros(1000) + wn = weight_utils.normalize_max_weight(weights, limit=0.01) + assert wn.max() <= 0.01 + + weights = torch.rand(1000) + wn = weight_utils.normalize_max_weight(weights, limit=0.02) + assert wn.max() <= 0.02 + + weights = torch.zeros(1000) + wn = weight_utils.normalize_max_weight(weights, limit=0.02) + assert wn.max() <= 0.02 + + weights = torch.rand(1000) + wn = weight_utils.normalize_max_weight(weights, limit=0.03) + assert wn.max() <= 0.03 + + weights = torch.zeros(1000) + wn = weight_utils.normalize_max_weight(weights, limit=0.03) + assert wn.max() <= 0.03 + + # Check for Limit + limit = 0.001 + weights = torch.rand(2000) + w = weights / weights.sum() + wn = weight_utils.normalize_max_weight(weights, limit=limit) + assert (w.max() >= limit and (limit - wn.max()).abs() < 0.001) or ( + w.max() < limit and wn.max() < limit + ) + + # Check for Zeros + limit = 0.01 + weights = torch.zeros(2000) + wn = weight_utils.normalize_max_weight(weights, limit=limit) + assert wn.max() == 1 / 2000 + + # Check for Ordering after normalization + weights = torch.rand(100) + wn = weight_utils.normalize_max_weight(weights, limit=1) + assert torch.isclose(wn, weights / weights.sum(), atol=1e-08, rtol=0).all() + + # Check for epsilon changes + epsilon = 0.01 + weights, _ = torch.sort(torch.rand(100)) + x = weights / weights.sum() + limit = x[-10] + change = epsilon * limit + y = weight_utils.normalize_max_weight(x, limit=limit - change) + z = weight_utils.normalize_max_weight(x, limit=limit + change) + assert (y - z).abs().sum() < epsilon + + +@pytest.mark.parametrize( + "test_id, n, uids, weights, expected", + [ + ("happy-path-1", 3, [0, 1, 2], [15, 5, 80], np.array([0.15, 0.05, 0.8])), + ("happy-path-2", 4, [1, 3], [50, 50], np.array([0.0, 0.5, 0.0, 0.5])), + ], +) +def test_convert_weight_uids_and_vals_to_tensor_happy_path( + test_id, n, uids, weights, expected +): + # Act + result = weight_utils.convert_weight_uids_and_vals_to_tensor(n, uids, weights) + + # Assert + assert np.allclose(result, expected), f"Failed {test_id}" + + +@pytest.mark.parametrize( + "test_id, n, uids, weights, subnets, expected", + [ + ( + "happy-path-1", + 3, + [0, 1, 2], + [15, 5, 80], + [0, 1, 2], + torch.tensor([0.15, 0.05, 0.8]), + ), + ( + "happy-path-2", + 3, + [0, 2], + [300, 300], + [0, 1, 2], + torch.tensor([0.5, 0.0, 0.5]), + ), + ], +) +def test_convert_weight_uids_and_vals_to_tensor_happy_path_torch( + test_id, n, uids, weights, subnets, expected, force_legacy_torch_compatible_api +): + # Act + result = weight_utils.convert_weight_uids_and_vals_to_tensor(n, uids, weights) + + # Assert + assert torch.allclose(result, expected), f"Failed {test_id}" + + +@pytest.mark.parametrize( + "test_id, n, uids, weights, expected", + [ + ("edge_case_empty", 5, [], [], np.zeros(5)), + ("edge_case_single", 1, [0], [100], np.array([1.0])), + ("edge_case_all_zeros", 4, [0, 1, 2, 3], [0, 0, 0, 0], np.zeros(4)), + ], +) +def test_convert_weight_uids_and_vals_to_tensor_edge_cases( + test_id, n, uids, weights, expected +): + # Act + result = weight_utils.convert_weight_uids_and_vals_to_tensor(n, uids, weights) + + # Assert + assert np.allclose(result, expected), f"Failed {test_id}" + + +@pytest.mark.parametrize( + "test_id, n, uids, weights, exception", + [ + ("error-case-mismatched-lengths", 3, [0, 1, 3, 4, 5], [10, 20, 30], IndexError), + ("error-case-negative-n", -1, [0, 1], [10, 20], ValueError), + ("error-case-invalid-uids", 3, [0, 3], [10, 20], IndexError), + ], +) +def test_convert_weight_uids_and_vals_to_tensor_error_cases( + test_id, n, uids, weights, exception +): + # Act / Assert + with pytest.raises(exception): + weight_utils.convert_weight_uids_and_vals_to_tensor(n, uids, weights) + + +@pytest.mark.parametrize( + "test_id, n, uids, weights, subnets, expected", + [ + ( + "happy-path-1", + 3, + [0, 1, 2], + [15, 5, 80], + [0, 1, 2], + np.array([0.15, 0.05, 0.8]), + ), + ( + "happy-path-2", + 3, + [0, 2], + [300, 300], + [0, 1, 2], + np.array([0.5, 0.0, 0.5]), + ), + ], +) +def test_convert_root_weight_uids_and_vals_to_tensor_happy_paths( + test_id, n, uids, weights, subnets, expected +): + # Act + result = weight_utils.convert_root_weight_uids_and_vals_to_tensor( + n, uids, weights, subnets + ) + + # Assert + assert np.allclose(result, expected, atol=1e-4), f"Failed {test_id}" + + +@pytest.mark.parametrize( + "test_id, n, uids, weights, subnets, expected", + [ + ( + "edge-1", + 1, + [0], + [0], + [0], + torch.tensor([0.0]), + ), # Single neuron with zero weight + ( + "edge-2", + 2, + [0, 1], + [0, 0], + [0, 1], + torch.tensor([0.0, 0.0]), + ), # All zero weights + ], +) +def test_convert_root_weight_uids_and_vals_to_tensor_edge_cases( + test_id, n, uids, weights, subnets, expected, force_legacy_torch_compatible_api +): + # Act + result = weight_utils.convert_root_weight_uids_and_vals_to_tensor( + n, uids, weights, subnets + ) + + # Assert + assert torch.allclose(result, expected, atol=1e-4), f"Failed {test_id}" + + +@pytest.mark.parametrize( + "test_id, n, uids, weights, subnets, expected", + [ + ( + "edge-1", + 1, + [0], + [0], + [0], + np.array([0.0]), + ), # Single neuron with zero weight + ( + "edge-2", + 2, + [0, 1], + [0, 0], + [0, 1], + np.array([0.0, 0.0]), + ), # All zero weights + ], +) +def test_convert_root_weight_uids_and_vals_to_tensor_edge_cases( + test_id, n, uids, weights, subnets, expected +): + # Act + result = weight_utils.convert_root_weight_uids_and_vals_to_tensor( + n, uids, weights, subnets + ) + + # Assert + assert np.allclose(result, expected, atol=1e-4), f"Failed {test_id}" + + +@pytest.mark.parametrize( + "test_id, n, uids, weights, subnets, exception", + [ + # uid not in subnets + ( + "error-1", + 3, + [1, 3], + [100, 200], + [1, 2], + "The subnet is unavailable at the moment.", + ), + # More uids than subnets + ( + "error-2", + 3, + [1, 2, 3], + [100, 200], + [1], + "The subnet is unavailable at the moment.", + ), + ], +) +def test_convert_root_weight_uids_and_vals_to_tensor_error_cases( + test_id, n, uids, weights, subnets, exception, caplog +): + with caplog.at_level(logging.WARNING): + weight_utils.convert_root_weight_uids_and_vals_to_tensor( + n, uids, weights, subnets + ) + + assert any( + exception in record.message and record.levelname == "WARNING" + for record in caplog.records + ) + + +@pytest.mark.parametrize( + "test_id, n, uids, bonds, expected_output", + [ + ( + "happy-path-1", + 5, + [1, 3, 4], + [10, 20, 30], + np.array([0, 10, 0, 20, 30], dtype=np.int64), + ), + ( + "happy-path-2", + 3, + [0, 1, 2], + [7, 8, 9], + np.array([7, 8, 9], dtype=np.int64), + ), + ("happy-path-3", 4, [2], [15], np.array([0, 0, 15, 0], dtype=np.int64)), + ], +) +def test_happy_path(test_id, n, uids, bonds, expected_output): + # Act + result = weight_utils.convert_bond_uids_and_vals_to_tensor(n, uids, bonds) + + # Assert + assert np.array_equal(result, expected_output), f"Failed {test_id}" + + +@pytest.mark.parametrize( + "test_id, n, uids, bonds, expected_output", + [ + ( + "happy-path-1", + 5, + [1, 3, 4], + [10, 20, 30], + torch.tensor([0, 10, 0, 20, 30], dtype=torch.int64), + ), + ( + "happy-path-2", + 3, + [0, 1, 2], + [7, 8, 9], + torch.tensor([7, 8, 9], dtype=torch.int64), + ), + ("happy-path-3", 4, [2], [15], torch.tensor([0, 0, 15, 0], dtype=torch.int64)), + ], +) +def test_happy_path_torch( + test_id, n, uids, bonds, expected_output, force_legacy_torch_compatible_api +): + # Act + result = weight_utils.convert_bond_uids_and_vals_to_tensor(n, uids, bonds) + + # Assert + assert torch.equal(result, expected_output), f"Failed {test_id}" + + +@pytest.mark.parametrize( + "test_id, n, uids, bonds, expected_output", + [ + ("edge-1", 1, [0], [0], np.array([0], dtype=np.int64)), # Single element + ( + "edge-2", + 10, + [], + [], + np.zeros(10, dtype=np.int64), + ), # Empty uids and bonds + ], +) +def test_edge_cases(test_id, n, uids, bonds, expected_output): + # Act + result = weight_utils.convert_bond_uids_and_vals_to_tensor(n, uids, bonds) + + # Assert + assert np.array_equal(result, expected_output), f"Failed {test_id}" + + +@pytest.mark.parametrize( + "test_id, n, uids, bonds, expected_output", + [ + ("edge-1", 1, [0], [0], torch.tensor([0], dtype=torch.int64)), # Single element + ( + "edge-2", + 10, + [], + [], + torch.zeros(10, dtype=torch.int64), + ), # Empty uids and bonds + ], +) +def test_edge_cases_torch( + test_id, n, uids, bonds, expected_output, force_legacy_torch_compatible_api +): + # Act + result = weight_utils.convert_bond_uids_and_vals_to_tensor(n, uids, bonds) + + # Assert + assert torch.equal(result, expected_output), f"Failed {test_id}" + + +@pytest.mark.parametrize( + "test_id, n, uids, bonds, exception", + [ + ("error-1", 5, [1, 3, 6], [10, 20, 30], IndexError), # uid out of bounds + ("error-2", -1, [0], [10], ValueError), # Negative number of neurons + ], +) +def test_error_cases(test_id, n, uids, bonds, exception): + # Act / Assert + with pytest.raises(exception): + weight_utils.convert_bond_uids_and_vals_to_tensor(n, uids, bonds) + + +def test_process_weights_for_netuid(mocker): + """Test the process_weights_for_netuid function.""" + # Prep + fake_uids = np.array([1, 2, 3, 4, 5], dtype=np.int64) + fake_weights = np.array([1.0, 2.5, 3.3, 4.7, 5.9], dtype=np.float32) + fake_netuid = 1 + fake_subtensor = mocker.MagicMock() + fake_metagraph = mocker.MagicMock() + fake_exclude_quantile = 0 + + fake_subtensor.min_allowed_weights.return_value = 0.1 + fake_subtensor.max_weight_limit.return_value = 1.0 + fake_metagraph.n = 1 + mocked_normalize_max_weight = mocker.patch.object( + weight_utils, "normalize_max_weight" + ) + + # Call + result = weight_utils.process_weights_for_netuid( + uids=fake_uids, + weights=fake_weights, + netuid=fake_netuid, + subtensor=fake_subtensor, + metagraph=fake_metagraph, + exclude_quantile=fake_exclude_quantile, + ) + + # Asserts + fake_subtensor.min_allowed_weights.assert_called_once_with(netuid=fake_netuid) + fake_subtensor.max_weight_limit.assert_called_once_with(netuid=fake_netuid) + + res1, res2 = result + assert np.array_equal(res1, fake_uids) + assert res2 == mocked_normalize_max_weight.return_value + + +def test_process_weights_with_all_zero_weights(mocker): + """Test the process_weights_for_netuid function with all zero weights.""" + # Prep + fake_uids = np.array([1, 2, 3, 4, 5], dtype=np.int64) + fake_weights = np.array([0.0, 0.0, 0.0, 0.0, 0.0], dtype=np.float32) + fake_netuid = 1 + fake_subtensor = mocker.MagicMock() + fake_metagraph = mocker.MagicMock() + fake_exclude_quantile = 0 + + fake_subtensor.min_allowed_weights.return_value = 0.1 + fake_subtensor.max_weight_limit.return_value = 1.0 + fake_metagraph.n = 1 + + # Call + result = weight_utils.process_weights_for_netuid( + uids=fake_uids, + weights=fake_weights, + netuid=fake_netuid, + subtensor=fake_subtensor, + metagraph=fake_metagraph, + exclude_quantile=fake_exclude_quantile, + ) + + # Asserts + fake_subtensor.min_allowed_weights.assert_called_once_with(netuid=fake_netuid) + fake_subtensor.max_weight_limit.assert_called_once_with(netuid=fake_netuid) + + res1, res2 = result + assert np.array_equal(res1, np.array([0])) + assert np.array_equal(res2, np.array([1.0])) + + +def test_process_weights_for_netuid_with_nzs_less_min_allowed_weights(mocker): + """Tests process_weights_for_netuid method when non-zero weights are less than the min allowed weights.""" + # Prep + fake_uids = np.array([1, 2, 3, 4, 5], dtype=np.int64) + fake_weights = np.array([0.1, 0.2, 0.3, 0.0, 0.0], dtype=np.float32) + fake_netuid = 1 + fake_subtensor = mocker.MagicMock() + fake_metagraph = None + fake_exclude_quantile = 0 + + fake_subtensor.min_allowed_weights.return_value = 4 + fake_subtensor.max_weight_limit.return_value = 1.0 + fake_subtensor.metagraph.return_value.n = 5 + mocked_np_arange = mocker.patch.object(np, "arange") + mocked_normalize_max_weight = mocker.patch.object( + weight_utils, "normalize_max_weight" + ) + + # Call + result = weight_utils.process_weights_for_netuid( + uids=fake_uids, + weights=fake_weights, + netuid=fake_netuid, + subtensor=fake_subtensor, + metagraph=fake_metagraph, + exclude_quantile=fake_exclude_quantile, + ) + + # Asserts + fake_subtensor.metagraph.assert_called_once_with(fake_netuid) + fake_subtensor.min_allowed_weights.assert_called_once_with(netuid=fake_netuid) + fake_subtensor.max_weight_limit.assert_called_once_with(netuid=fake_netuid) + assert result == ( + mocked_np_arange.return_value, + mocked_normalize_max_weight.return_value, + ) + + +def test_generate_weight_hash(mocker): + """Tests weight_utils.generate_weight_hash function.""" + # Prep + fake_address = "5D1ABCD" + fake_netuid = 1 + fake_uids = [1, 2] + fake_values = [10, 20] + fake_version_key = 80000 + fake_salt = [1, 2] + + mocked_scale_bytes = mocker.patch.object(weight_utils, "ScaleBytes") + mocked_keypair = mocker.patch.object(weight_utils, "Keypair") + mocker_vec = mocker.patch.object(weight_utils, "Vec") + mocked_u16 = mocker.patch.object(weight_utils, "U16") + mocked_hasher = mocker.patch.object(weight_utils.hashlib, "blake2b") + + # Call + result = weight_utils.generate_weight_hash( + address=fake_address, + netuid=fake_netuid, + uids=fake_uids, + values=fake_values, + version_key=fake_version_key, + salt=fake_salt, + ) + + # Asserts + mocked_scale_bytes.assert_called() + mocked_keypair.assert_called() + mocker_vec.assert_called() + mocked_u16.assert_called() + assert ( + result + == mocked_hasher.return_value.hexdigest.return_value.__radd__.return_value + ) diff --git a/tools/capitalist.py b/tools/capitalist.py deleted file mode 100755 index 369a3389fa..0000000000 --- a/tools/capitalist.py +++ /dev/null @@ -1,229 +0,0 @@ -#!/bin/python3 - -from argparse import ArgumentParser - -from bittensor.balance import Balance -from bittensor.subtensor.client import WSClient -from bittensor.subtensor.interface import Keypair -from loguru import logger -import json -from bittensor.crypto import is_encrypted, decrypt_data, KeyError -from bittensor.crypto.keyfiles import load_keypair_from_data, KeyFileError -from bittensor.utils import Cli -import asyncio -import sys - -import os - -from prettytable import PrettyTable -from bittensor.subtensor.client import Neuron, Neurons - - -class CommandExecutor: - __keypair : Keypair - __client : WSClient - def __init__(self, keypair : Keypair, client : WSClient): - self.__keypair = keypair - self.__client = client - - async def connect(self): - self.__client.connect() - await self.__client.is_connected() - - async def _associated_neurons(self) -> Neurons: - pubkey = self.__keypair.public_key - - logger.debug("Retrieving all nodes associated with cold key : {}", pubkey) - - neurons = await self.__client.neurons(decorator=True) - - result = filter(lambda x : x.coldkey == pubkey, neurons )# These are the neurons associated with the provided cold key - - associated_neurons = Neurons(result) - - # Load stakes - for neuron in associated_neurons: - neuron.stake = await self.__client.get_stake_for_uid(neuron.uid) - - - return associated_neurons - - - async def overview(self): - await self.connect() - balance = await self.__client.get_balance(self.__keypair.ss58_address) - neurons = await self._associated_neurons() - - print("BALANCE: %s : [%s]" % (self.__keypair.ss58_address, balance)) - print() - print("--===[[ STAKES ]]===--") - t = PrettyTable(["UID", "IP", "STAKE"]) - t.align = 'l' - for neuron in neurons: - t.add_row([neuron.uid, neuron.ip, neuron.stake]) - - print(t.get_string()) - - async def unstake_all(self): - await self.connect() - neurons = await self._associated_neurons() - for neuron in neurons: - neuron.stake = await self.__client.get_stake_for_uid(neuron.uid) - await self.__client.unstake(neuron.stake, neuron.hotkey) - - async def unstake(self, uid, amount : Balance): - await self.connect() - neurons = await self._associated_neurons() - neuron = neurons.get_by_uid(uid) - if not neuron: - logger.error("Neuron with uid {} is not associated with this cold key") - quit() - - neuron.stake = await self.__client.get_stake_for_uid(uid) - if amount > neuron.stake: - logger.error("Neuron with uid {} does not have enough stake ({}) to be able to unstake {}", uid, neuron.stake, amount) - quit() - - await self.__client.unstake(amount, neuron.hotkey) - - async def stake(self, uid, amount : Balance): - await self.connect() - balance = await self.__client.get_balance(self.__keypair.ss58_address) - if balance < amount: - logger.error("Not enough balance ({}) to stake {}", balance, amount) - - neurons = await self._associated_neurons() - neuron = neurons.get_by_uid(uid) - if not neuron: - logger.error("Neuron with uid {} is not associated with this cold key") - quit() - - await self.__client.add_stake(amount, neuron.hotkey) - - - - - - -def __validate_path(path): - path = os.path.expanduser(path) - - if not os.path.isfile(path): - logger.error("{} is not a file. Aborting", path) - quit() - - if not os.access(path, os.R_OK): - logger.error("{} is not readable. Aborting", path) - quit() - -def load_key(path) -> Keypair: - path = os.path.expanduser(path) - try: - with open(path, 'rb') as file: - data = file.read() - if is_encrypted(data): - password = Cli.ask_password() - data = decrypt_data(password, data) - - return load_keypair_from_data(data) - - except KeyError: - logger.error("Invalid password") - quit() - except KeyFileError as e: - logger.error("Keyfile corrupt") - raise e - -def get_client(endpoint, keypair): - return WSClient(socket=endpoint, keypair=keypair) - -def enable_debug(should_debug): - if not should_debug: - logger.remove() - logger.add(sink=sys.stderr, level="INFO") - - - - -''' - -Functions : -- Generate cold key -- View balance -- View hotkeys associated with the supplied cold key -- Stake funds into hotkey ( one by one / amount devided equally over keys) -- Unstake funds into coldkey (one by one / withdraw all) - - -''' - -def main(): - parser = ArgumentParser(description="Capitalism yeey") - parser.add_argument("--chain-endpoint", default="feynman.kusanagi.bittensor.com:9944", required=False, help="The endpoint to the subtensor chain :") - parser.add_argument("--cold-key", default='~/.bittensor/cold_key', help="Path to the cold key") - parser.add_argument("--debug", default=False, help="Turn on debugging information", action="store_true") - - cmd_parsers = parser.add_subparsers(dest='command', required=True) - overview_parser = cmd_parsers.add_parser('overview') - unstake_parser = cmd_parsers.add_parser('unstake') - unstake_parser.add_argument('--all', dest="unstake_all", action='store_true') - unstake_parser.add_argument('--uid', dest="uid", type=int, required=False) - unstake_parser.add_argument('--amount', dest="amount", type=float, required=False) - - stake_parser = cmd_parsers.add_parser('stake') - stake_parser.add_argument('--uid', dest="uid", type=int, required=False) - stake_parser.add_argument('--amount', dest="amount", type=float, required=False) - - - args = parser.parse_args() - - endpoint = args.chain_endpoint - enable_debug(args.debug) - - __validate_path(args.cold_key) - keypair = load_key(args.cold_key) - - client = get_client(endpoint, keypair) - executor = CommandExecutor(keypair, client) - loop = asyncio.get_event_loop() - - if args.command == "overview": - loop.run_until_complete(executor.overview()) - - if args.command == "unstake": - if args.unstake_all: - confirm = input("This will remove all stake from associated neurons, and transfer the balance in the account associated with the cold key. Continue? (y/N) ") - if confirm not in (["Y", 'y']): - quit() - loop.run_until_complete(executor.unstake_all()) - quit() - - - if not args.uid: - logger.error("The --uid argument is required for this command") - quit() - - if not args.amount: - logger.error("The --amount argument is required for this command") - quit() - - amount = Balance.from_float(args.amount) - - loop.run_until_complete(executor.unstake(args.uid, amount)) - - if args.command == "stake": - if not args.uid: - logger.error("The --uid argument is required for this command") - quit() - - if not args.amount: - logger.error("The --amount argument is required for this command") - quit() - - amount = Balance.from_float(args.amount) - loop.run_until_complete(executor.stake(args.uid, amount)) - - - -if __name__ == '__main__': - main() \ No newline at end of file diff --git a/tools/genkey.py b/tools/genkey.py deleted file mode 100755 index cd65b28fe1..0000000000 --- a/tools/genkey.py +++ /dev/null @@ -1,197 +0,0 @@ -#!/bin/python3 - -from bittensor.subtensor.interface import Keypair -from termcolor import colored -from bittensor.crypto import encrypt - -from argparse import ArgumentParser -import json -import os -import stat -from password_strength import PasswordPolicy -import getpass - - -def create_config_dir_if_not_exists(): - config_dir = "~/.bittensor" - config_dir = os.path.expanduser(config_dir) - if os.path.exists(config_dir): - if os.path.isdir(config_dir): - return - else: - print(colored("~/.bittensor exists, but is not a directory. Aborting", 'red')) - quit() - - os.mkdir(config_dir) - -def may_overwrite(file): - choice = input("File %s already exists. Overwrite ? (y/N) " % file) - if choice == "y": - return True - else: - return False - -def validate_path(keyfile): - keyfile = os.path.expanduser(keyfile) - - if os.path.isfile(keyfile): - if os.access(keyfile, os.W_OK): - if may_overwrite(keyfile): - return keyfile - else: - quit() - else: - print(colored("No write access for %s" % keyfile, 'red')) - quit() - else: - pdir = os.path.dirname(keyfile) - if os.access(pdir, os.W_OK): - return keyfile - else: - print(colored("No write access for %s" % keyfile, 'red')) - quit() - -def input_password(): - valid = False - while not valid: - password = getpass.getpass("Specify password for key encryption: ") - valid = validate_password(password) - - return password - -def validate_password(password): - policy = PasswordPolicy.from_names( - strength=0.66, - entropybits=30, - length=8, - ) - - if not password: - return False - - tested_pass = policy.password(password) - result = tested_pass.test() - if len(result) > 0: - print(colored('Password not strong enough. Try increasing the length of the password or the password comlexity')) - return False - - - password_verification = getpass.getpass("Retype your password: ") - if password != password_verification: - print("Passwords do not match") - return False - - return True - -def validate_generate_mnemonic(mnemonic): - if len(mnemonic) not in [12,15,18,21,24]: - print(colored("Mnemonic has invalid size. This should be 12,15,18,21 or 24 words", 'red')) - quit() - - try: - keypair = Keypair.create_from_mnemonic(" ".join(mnemonic)) - return keypair - except ValueError as e: - print(colored(str(e), "red")) - quit() - - - -def gen_new_key(words): - mnemonic = Keypair.generate_mnemonic(words) - keypair = Keypair.create_from_mnemonic(mnemonic) - - return keypair - - -def display_mnemonic_msg(kepair : Keypair): - mnemonic =kepair.mnemonic - mnemonic_green = colored(mnemonic, 'green') - print ("The mnemonic to the new key is:\n\n%s\n" % mnemonic_green) - print ("You can use the mnemonic to recreate the key in case it gets lost. The command to use to regenerate the key using this mnemonic is:") - print("python3 ./genkey.py regen --mnemonic %s" % mnemonic) - print('') - print (colored("It is important to store this mnemonic in a secure (preferable offline place), as anyone " \ - "who has possesion of this mnemonic can use it to regenerate the key and access your tokens", "white")) - - - -def save_keys(path, data): - print("Writing key to %s" % path) - with open(path, "wb") as keyfile: - keyfile.write(data) - -def set_file_permissions(path): - os.chmod(path, stat.S_IRUSR | stat.S_IWUSR) - pass - -def confirm_no_password(): - print(colored('*** WARNING ***', 'white')) - print(colored('You have not specified the --password flag.', 'white')) - print(colored('This means that the generated key will be stored as plaintext in the keyfile', 'white')) - print(colored('The benefit of this is that you will not be prompted for a password when bittensor starts', 'white')) - print(colored('The drawback is that an attacker has access to the key if they have access to the account bittensor runs on', 'white')) - print() - choice = input("Do you wish to proceed? (Y/n) ") - if choice in ["n", "N"]: - return False - - return True - - - -def main(): - create_config_dir_if_not_exists() - - parser = ArgumentParser(description="Generate a key for bittensor") - cmd_parsers = parser.add_subparsers(dest='command', required=True) - - new_key_parser = cmd_parsers.add_parser('new') - new_key_parser.add_argument('--words', - type=int, - choices=[12,15,18,21,24], - default=12, - help="The amount of words the mnemonic representing the key will contain") - new_key_parser.add_argument('--password', action='store_true', help='Protect the generated bittensor key with a password') - new_key_parser.add_argument('--keyfile', help='The destination path of the keyfile (default: ~/.bittensor/keys)', - default='~/.bittensor/key') - - regen_key_parser = cmd_parsers.add_parser('regen') - regen_key_parser.add_argument("--mnemonic", required=True, nargs="+") - regen_key_parser.add_argument('--password', action='store_true', help='Protect the generated bittensor key with a password') - regen_key_parser.add_argument('--file', help='The destination path of the keyfile (default: ~/.bittensor/keys)', - default='~/.bittensor/key') - - - - - args = parser.parse_args() - keyfile = validate_path(args.keyfile) - - if not args.password and not confirm_no_password(): - quit() - - if args.command == 'new': - keypair = gen_new_key(args.words) - display_mnemonic_msg(keypair) - else: # Implies regen - keypair = validate_generate_mnemonic(args.mnemonic) - - data = json.dumps(keypair.toDict()).encode() - - if args.password: - password = input_password() - print("Encrypting key, this might take a while") - data = encrypt(data, password) - - - save_keys(keyfile, data) - set_file_permissions(keyfile) - -if __name__ == '__main__': - main() - - - - -