Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
24e1fdd
fix(axon): narrow preprocess exception handling and chain causes
RUNECTZ33 May 12, 2026
8c481fe
move `no_parse_cli` to settings.py
basfroman May 13, 2026
514ad45
improve check `no_parse_cli` logic in logmachine and config
basfroman May 13, 2026
0e20b59
call function to have up-to-date result always
basfroman May 13, 2026
ba271e4
fix test + add test for user script
basfroman May 13, 2026
d5dbe8f
opps, remove debug
basfroman May 13, 2026
9154e03
remove unused import
basfroman May 13, 2026
6a0829b
invert arguments
basfroman May 13, 2026
e43acb4
pass env var value to e2e tests
basfroman May 13, 2026
d704968
fix missed Validator's setup
basfroman May 13, 2026
f97108b
add time for chain update after unstake with fast blocks e2e test
basfroman May 13, 2026
2a8bec5
Merge pull request #3347 from latent-to/feat/roman/no_parse_cli-impro…
basfroman May 13, 2026
ad19b5f
Merge branch 'staging' into fix/roman/flaky-test
basfroman May 13, 2026
a73e3de
Merge pull request #3348 from latent-to/fix/roman/flaky-test
basfroman May 13, 2026
f813a3f
Merge branch 'staging' into fix/runectz33/axon-exception-narrowing
basfroman May 13, 2026
63da714
Merge pull request #3346 from RUNECTZ33/fix/runectz33/axon-exception-…
basfroman May 14, 2026
53f8a9a
each sign_and_send_extrinsic call previously triggered create_signed_…
thewhaleking May 14, 2026
e1d849a
Revert and apply only to swaps
thewhaleking May 14, 2026
9d24be0
Mock test fix
thewhaleking May 14, 2026
a03b7b5
run do_take_checks at pool validation
thewhaleking May 14, 2026
f1ca9d8
run do_take_checks at pool validation (#3350)
thewhaleking May 14, 2026
1033a28
Merge branch 'staging' into fix/thewhaleking/mev-shield-nonce
basfroman May 14, 2026
82a3247
Merge pull request #3349 from latent-to/fix/thewhaleking/mev-shield-n…
thewhaleking May 14, 2026
0e91511
Use the public API
thewhaleking May 14, 2026
35992fd
Ruff
thewhaleking May 14, 2026
ce734c9
Async test_commit_weights.py fix
thewhaleking May 15, 2026
5bebfbd
Clear nonce cache on extrinsic submission failure
thewhaleking May 15, 2026
c27bc62
Merge pull request #3352 from latent-to/fix/thewhaleking/subtensor-up…
thewhaleking May 15, 2026
092cbd4
Merge pull request #3351 from latent-to/fix/thewhaleking/mev-shield-n…
thewhaleking May 15, 2026
3bf570c
Version + changelog
thewhaleking May 15, 2026
77b905f
Merge pull request #3353 from latent-to/changelog/10.3.2
thewhaleking May 15, 2026
6121196
Merge branch 'master' into release/10.3.2
thewhaleking May 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
# Changelog

## 10.3.2 /2026-05-15

## What's Changed
* Fix `--help` hijacking on import bittensor by @basfroman in https://github.com/latent-to/bittensor/pull/3347
* Hopefully, this is a fix for the flaky e2e test by @basfroman in https://github.com/latent-to/bittensor/pull/3348
* fix(axon): narrow preprocess exception handling and chain causes by @RUNECTZ33 in https://github.com/latent-to/bittensor/pull/3346
* run do_take_checks at pool validation by @thewhaleking in https://github.com/latent-to/bittensor/pull/3350
* Mev Shield Nonce Increment Fix by @thewhaleking in https://github.com/latent-to/bittensor/pull/3349
* Test Fixes by @thewhaleking in https://github.com/latent-to/bittensor/pull/3352
* Use the public API for clearing nonce cache by @thewhaleking in https://github.com/latent-to/bittensor/pull/3351

## New Contributors
* @RUNECTZ33 made their first contribution in https://github.com/latent-to/bittensor/pull/3346

**Full Changelog**: https://github.com/latent-to/bittensor/compare/v10.3.1...v10.3.2

## 10.3.1 /2026-05-06

## What's Changed
Expand Down
15 changes: 13 additions & 2 deletions bittensor/core/async_subtensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,11 @@
decode_revealed_commitment_with_hotkey,
)
from bittensor.core.config import Config
from bittensor.core.errors import ChainError, SubstrateRequestException
from bittensor.core.errors import (
ChainError,
SubstrateRequestException,
chain_error_from_substrate_exception,
)
from bittensor.core.extrinsics.asyncex.children import (
root_set_pending_childkey_cooldown_extrinsic,
set_children_extrinsic,
Expand Down Expand Up @@ -6145,8 +6149,15 @@ async def sign_and_send_extrinsic(
return extrinsic_response

except SubstrateRequestException as error:
# The extrinsic was rejected before inclusion (pool validation, dropped,
# invalid, etc.), so the nonce was not consumed on-chain. Clear the cached
# next-index so the next call refetches the true on-chain value.
self.substrate.clear_nonce_cache_for_account(signing_keypair.ss58_address)
typed = chain_error_from_substrate_exception(error)
if typed is not None:
error = typed
if raise_error:
raise
raise error from None

extrinsic_response.success = False
extrinsic_response.message = format_error_message(error)
Expand Down
20 changes: 15 additions & 5 deletions bittensor/core/axon.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from typing import Any, Awaitable, Callable, Optional, Tuple

from async_substrate_interface.utils import json
from pydantic import ValidationError
import uvicorn
from bittensor_wallet import Wallet, Keypair
from fastapi import APIRouter, Depends, FastAPI
Expand Down Expand Up @@ -1233,13 +1234,17 @@ async def preprocess(self, request: "Request") -> "Synapse":
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.
# Extracts the request name from the URL path. `split("/")[1]` can
# only realistically fail with `IndexError` (empty path) or
# `AttributeError` (malformed/mocked request without a `url`); a bare
# `except Exception` here would swallow `MemoryError`, real bugs from
# `starlette`, etc., and relabel them as malformed-request errors.
try:
request_name = request.url.path.split("/")[1]
except Exception:
except (IndexError, AttributeError) as e:
raise InvalidRequestNameError(
f"Improperly formatted request. Could not parser request {request.url.path}."
)
) from e

# Creates a synapse instance from the headers using the appropriate forward class type
# based on the request name obtained from the URL path.
Expand All @@ -1249,12 +1254,17 @@ async def preprocess(self, request: "Request") -> "Synapse":
f"Synapse name '{request_name}' not found. Available synapses {list(self.axon.forward_class_types.keys())}"
)

# `from_headers` constructs a pydantic model from header inputs; the
# realistic failure modes are `ValidationError` (field-level),
# `TypeError` (bad kwarg types), and `ValueError` (custom validators).
# Narrowing keeps unrelated infrastructure failures visible instead of
# relabeling them as malformed-request errors.
try:
synapse = request_synapse.from_headers(request.headers) # type: ignore
except Exception:
except (ValidationError, TypeError, ValueError) as e:
raise SynapseParsingError(
f"Improperly formatted request. Could not parse headers {request.headers} into synapse of type {request_name}."
)
) from e
synapse.name = request_name

# Fills the local axon information into the synapse.
Expand Down
12 changes: 4 additions & 8 deletions bittensor/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,11 @@
import sys
from copy import deepcopy
from typing import Any, Optional
from bittensor.core.settings import DEFAULTS

import yaml

from bittensor.core.settings import DEFAULTS, no_parse_cli


class DefaultMunch(dict):
"""
Expand Down Expand Up @@ -130,12 +132,6 @@ def __init__(
strict: bool = False,
default: Any = DEFAULTS,
) -> None:
no_parse_cli = os.getenv("BT_NO_PARSE_CLI_ARGS", "").lower() in (
"1",
"true",
"yes",
"on",
)

# Fallback to defaults if not provided
default = deepcopy(default or DEFAULTS)
Expand All @@ -153,7 +149,7 @@ def __init__(
self.__is_set = {}

# If CLI parsing disabled, stop here
if no_parse_cli or parser is None:
if no_parse_cli() or parser is None:
return

self._add_default_arguments(parser)
Expand Down
53 changes: 53 additions & 0 deletions bittensor/core/errors.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import ast
from typing import Optional, TYPE_CHECKING

from async_substrate_interface.errors import (
Expand Down Expand Up @@ -58,6 +59,7 @@
"UnknownSynapseError",
"UnstakeError",
"SHIELD_VALIDATION_ERRORS",
"chain_error_from_substrate_exception",
"map_shield_error",
]

Expand Down Expand Up @@ -278,6 +280,57 @@ def __init__(
}


_CUSTOM_ERROR_CODE_TO_EXCEPTION: dict[int, type["ChainError"]] = {
3: SubnetNotExists,
4: HotKeyAccountNotExists,
6: TxRateLimitExceeded,
25: NonAssociatedColdKey,
}


def _extract_custom_error_code(error: Exception) -> Optional[int]:
"""Walk a SubstrateRequestException's args looking for a transaction-pool
validity error like ``{'error': {'code': 1010, ..., 'data': 'Custom error: N'}}``
and return ``N``. Returns ``None`` if the shape doesn't match."""
for arg in getattr(error, "args", ()):
parsed = arg
if isinstance(parsed, str):
try:
parsed = ast.literal_eval(parsed)
except (ValueError, SyntaxError, MemoryError, RecursionError, TypeError):
continue
if not isinstance(parsed, dict):
continue
inner = parsed.get("error", parsed)
if not isinstance(inner, dict):
continue
data = inner.get("data")
if isinstance(data, str) and data.startswith("Custom error:"):
try:
return int(data.split(":", 1)[1].strip())
except ValueError:
continue
return None


def chain_error_from_substrate_exception(
error: SubstrateRequestException,
) -> Optional["ChainError"]:
"""Translate a transaction-pool validation error into a typed
:class:`ChainError`. Returns ``None`` when the exception doesn't carry a
recognised ``Custom error: N`` code from subtensor's
``CustomTransactionError`` enum, so callers can keep the original."""
if isinstance(error, ChainError):
return None
code = _extract_custom_error_code(error)
if code is None:
return None
exc_cls = _CUSTOM_ERROR_CODE_TO_EXCEPTION.get(code)
if exc_cls is None:
return None
return exc_cls(*error.args)


def map_shield_error(raw_message: str) -> str:
"""Map a raw shield validation error to a human-readable description.

Expand Down
7 changes: 7 additions & 0 deletions bittensor/core/extrinsics/asyncex/coldkey_swap.py
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,13 @@ async def swap_coldkey_announced_extrinsic(
)

if response.success:
# The swap may reap the old coldkey's account on chain (resetting its
# nonce). Drop the in-process nonce cache entry so the next extrinsic
# signed by this ss58 re-fetches from the chain instead of incrementing
# a stale value.
subtensor.substrate.clear_nonce_cache_for_account(
wallet.coldkeypub.ss58_address
)
logging.debug("[green]Coldkey swap executed successfully.[/green]")
else:
logging.error(f"[red]{response.message}[/red]")
Expand Down
9 changes: 8 additions & 1 deletion bittensor/core/extrinsics/asyncex/sudo.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ async def swap_coldkey_extrinsic(
Notes:
- This function can only called by root.
"""
return await sudo_call_extrinsic(
response = await sudo_call_extrinsic(
subtensor=subtensor,
wallet=wallet,
call_module="SubtensorModule",
Expand All @@ -108,6 +108,13 @@ async def swap_coldkey_extrinsic(
wait_for_inclusion=wait_for_inclusion,
wait_for_finalization=wait_for_finalization,
)
if response.success:
# The swap may reap the old coldkey's account on chain (resetting its
# nonce). Drop the in-process nonce cache entry so the next extrinsic
# signed by this ss58 re-fetches from the chain instead of incrementing
# a stale value.
subtensor.substrate.clear_nonce_cache_for_account(old_coldkey_ss58)
return response


async def sudo_set_admin_freeze_window_extrinsic(
Expand Down
10 changes: 10 additions & 0 deletions bittensor/core/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,3 +166,13 @@ class wallet:
e * (_version_int_base**i) for i, e in enumerate(reversed(_version_info))
)
assert version_as_int < 2**31 # fits in int32


# used for backwards compatibility in config.py and loggingmachine.py
def no_parse_cli():
return not os.getenv("BT_NO_PARSE_CLI_ARGS", "true").lower() in (
"0",
"false",
"no",
"off",
)
7 changes: 5 additions & 2 deletions bittensor/core/subtensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
decode_revealed_commitment_with_hotkey,
)
from bittensor.core.config import Config
from bittensor.core.errors import ChainError
from bittensor.core.errors import ChainError, chain_error_from_substrate_exception
from bittensor.core.extrinsics.children import (
root_set_pending_childkey_cooldown_extrinsic,
set_children_extrinsic,
Expand Down Expand Up @@ -5012,8 +5012,11 @@ def sign_and_send_extrinsic(
return extrinsic_response

except SubstrateRequestException as error:
typed = chain_error_from_substrate_exception(error)
if typed is not None:
error = typed
if raise_error:
raise
raise error from None

extrinsic_response.success = False
extrinsic_response.message = format_error_message(error)
Expand Down
4 changes: 3 additions & 1 deletion bittensor/utils/btlogging/loggingmachine.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from statemachine import State, StateMachine

from bittensor.core.config import Config
from bittensor.core.settings import DEFAULTS
from bittensor.core.settings import DEFAULTS, no_parse_cli
from bittensor.utils.btlogging.console import BittensorConsole
from .defines import (
BITTENSOR_LOGGER_NAME,
Expand Down Expand Up @@ -686,6 +686,8 @@ def config(cls) -> "Config":
Return:
Configuration object with settings from command-line arguments.
"""
if no_parse_cli():
return Config()
parser = argparse.ArgumentParser()
cls.add_args(parser)
return Config(parser)
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "bittensor"
version = "10.3.1"
version = "10.3.2"
description = "Bittensor SDK"
readme = "README.md"
authors = [
Expand Down Expand Up @@ -33,7 +33,7 @@ dependencies = [
"uvicorn",
"bittensor-drand>=1.3.0,<2.0.0",
"bittensor-wallet==4.0.1",
"async-substrate-interface>=2.0.3,<3.0.0",
"async-substrate-interface>=2.0.4,<3.0.0",
]

[project.optional-dependencies]
Expand Down
5 changes: 5 additions & 0 deletions tests/e2e_tests/test_commit_weights.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,11 @@ async def test_commit_and_reveal_weights_legacy_async(async_subtensor, alice_wal
> 0
), "Invalid RevealPeriodEpochs"

# Wait until the reveal block range
await async_subtensor.wait_for_block(
await async_subtensor.subnets.get_next_epoch_start_block(alice_sn.netuid) + 1
)

# Reveal weights
success, message = await async_subtensor.extrinsics.reveal_weights(
wallet=alice_wallet,
Expand Down
6 changes: 6 additions & 0 deletions tests/e2e_tests/test_staking.py
Original file line number Diff line number Diff line change
Expand Up @@ -1737,6 +1737,9 @@ def test_unstaking_with_limit(
rate_tolerance=rate_tolerance,
).success

# time for chain update
subtensor.wait_for_block()

# Make sure both unstake were successful.
bob_stakes = subtensor.staking.get_stake_info_for_coldkey(
bob_wallet.coldkey.ss58_address
Expand Down Expand Up @@ -1835,6 +1838,9 @@ async def test_unstaking_with_limit_async(
)
).success

# time for chain update
await async_subtensor.wait_for_block()

# Make sure both unstake were successful.
bob_stakes = await async_subtensor.staking.get_stake_info_for_coldkey(
bob_wallet.coldkey.ss58_address
Expand Down
2 changes: 2 additions & 0 deletions tests/e2e_tests/utils/e2e_test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ def __exit__(self, exc_type, exc_value, traceback):
async def __aenter__(self):
env = os.environ.copy()
env["BT_LOGGING_DEBUG"] = "1"
env["BT_NO_PARSE_CLI_ARGS"] = "false"
self.process = await asyncio.create_subprocess_exec(
sys.executable,
f"{self.dir}/miner.py",
Expand Down Expand Up @@ -195,6 +196,7 @@ def __init__(self, dir, wallet, netuid):
async def __aenter__(self):
env = os.environ.copy()
env["BT_LOGGING_DEBUG"] = "1"
env["BT_NO_PARSE_CLI_ARGS"] = "false"
self.process = await asyncio.create_subprocess_exec(
sys.executable,
f"{self.dir}/validator.py",
Expand Down
Loading
Loading