Skip to content
This repository was archived by the owner on Jul 10, 2026. It is now read-only.

Commit b90225e

Browse files
authored
Merge pull request #3354 from latent-to/release/10.3.2
Release/10.3.2
2 parents ac742be + 6121196 commit b90225e

18 files changed

Lines changed: 269 additions & 24 deletions

File tree

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,21 @@
11
# Changelog
22

3+
## 10.3.2 /2026-05-15
4+
5+
## What's Changed
6+
* Fix `--help` hijacking on import bittensor by @basfroman in https://github.com/latent-to/bittensor/pull/3347
7+
* Hopefully, this is a fix for the flaky e2e test by @basfroman in https://github.com/latent-to/bittensor/pull/3348
8+
* fix(axon): narrow preprocess exception handling and chain causes by @RUNECTZ33 in https://github.com/latent-to/bittensor/pull/3346
9+
* run do_take_checks at pool validation by @thewhaleking in https://github.com/latent-to/bittensor/pull/3350
10+
* Mev Shield Nonce Increment Fix by @thewhaleking in https://github.com/latent-to/bittensor/pull/3349
11+
* Test Fixes by @thewhaleking in https://github.com/latent-to/bittensor/pull/3352
12+
* Use the public API for clearing nonce cache by @thewhaleking in https://github.com/latent-to/bittensor/pull/3351
13+
14+
## New Contributors
15+
* @RUNECTZ33 made their first contribution in https://github.com/latent-to/bittensor/pull/3346
16+
17+
**Full Changelog**: https://github.com/latent-to/bittensor/compare/v10.3.1...v10.3.2
18+
319
## 10.3.1 /2026-05-06
420

521
## What's Changed

bittensor/core/async_subtensor.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,11 @@
4848
decode_revealed_commitment_with_hotkey,
4949
)
5050
from bittensor.core.config import Config
51-
from bittensor.core.errors import ChainError, SubstrateRequestException
51+
from bittensor.core.errors import (
52+
ChainError,
53+
SubstrateRequestException,
54+
chain_error_from_substrate_exception,
55+
)
5256
from bittensor.core.extrinsics.asyncex.children import (
5357
root_set_pending_childkey_cooldown_extrinsic,
5458
set_children_extrinsic,
@@ -6145,8 +6149,15 @@ async def sign_and_send_extrinsic(
61456149
return extrinsic_response
61466150

61476151
except SubstrateRequestException as error:
6152+
# The extrinsic was rejected before inclusion (pool validation, dropped,
6153+
# invalid, etc.), so the nonce was not consumed on-chain. Clear the cached
6154+
# next-index so the next call refetches the true on-chain value.
6155+
self.substrate.clear_nonce_cache_for_account(signing_keypair.ss58_address)
6156+
typed = chain_error_from_substrate_exception(error)
6157+
if typed is not None:
6158+
error = typed
61486159
if raise_error:
6149-
raise
6160+
raise error from None
61506161

61516162
extrinsic_response.success = False
61526163
extrinsic_response.message = format_error_message(error)

bittensor/core/axon.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from typing import Any, Awaitable, Callable, Optional, Tuple
1616

1717
from async_substrate_interface.utils import json
18+
from pydantic import ValidationError
1819
import uvicorn
1920
from bittensor_wallet import Wallet, Keypair
2021
from fastapi import APIRouter, Depends, FastAPI
@@ -1233,13 +1234,17 @@ async def preprocess(self, request: "Request") -> "Synapse":
12331234
This method sets the foundation for the subsequent steps in the request handling process,
12341235
ensuring that all necessary information is encapsulated within the Synapse object.
12351236
"""
1236-
# Extracts the request name from the URL path.
1237+
# Extracts the request name from the URL path. `split("/")[1]` can
1238+
# only realistically fail with `IndexError` (empty path) or
1239+
# `AttributeError` (malformed/mocked request without a `url`); a bare
1240+
# `except Exception` here would swallow `MemoryError`, real bugs from
1241+
# `starlette`, etc., and relabel them as malformed-request errors.
12371242
try:
12381243
request_name = request.url.path.split("/")[1]
1239-
except Exception:
1244+
except (IndexError, AttributeError) as e:
12401245
raise InvalidRequestNameError(
12411246
f"Improperly formatted request. Could not parser request {request.url.path}."
1242-
)
1247+
) from e
12431248

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

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

12601270
# Fills the local axon information into the synapse.

bittensor/core/config.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,11 @@
2121
import sys
2222
from copy import deepcopy
2323
from typing import Any, Optional
24-
from bittensor.core.settings import DEFAULTS
24+
2525
import yaml
2626

27+
from bittensor.core.settings import DEFAULTS, no_parse_cli
28+
2729

2830
class DefaultMunch(dict):
2931
"""
@@ -130,12 +132,6 @@ def __init__(
130132
strict: bool = False,
131133
default: Any = DEFAULTS,
132134
) -> None:
133-
no_parse_cli = os.getenv("BT_NO_PARSE_CLI_ARGS", "").lower() in (
134-
"1",
135-
"true",
136-
"yes",
137-
"on",
138-
)
139135

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

155151
# If CLI parsing disabled, stop here
156-
if no_parse_cli or parser is None:
152+
if no_parse_cli() or parser is None:
157153
return
158154

159155
self._add_default_arguments(parser)

bittensor/core/errors.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import ast
12
from typing import Optional, TYPE_CHECKING
23

34
from async_substrate_interface.errors import (
@@ -58,6 +59,7 @@
5859
"UnknownSynapseError",
5960
"UnstakeError",
6061
"SHIELD_VALIDATION_ERRORS",
62+
"chain_error_from_substrate_exception",
6163
"map_shield_error",
6264
]
6365

@@ -278,6 +280,57 @@ def __init__(
278280
}
279281

280282

283+
_CUSTOM_ERROR_CODE_TO_EXCEPTION: dict[int, type["ChainError"]] = {
284+
3: SubnetNotExists,
285+
4: HotKeyAccountNotExists,
286+
6: TxRateLimitExceeded,
287+
25: NonAssociatedColdKey,
288+
}
289+
290+
291+
def _extract_custom_error_code(error: Exception) -> Optional[int]:
292+
"""Walk a SubstrateRequestException's args looking for a transaction-pool
293+
validity error like ``{'error': {'code': 1010, ..., 'data': 'Custom error: N'}}``
294+
and return ``N``. Returns ``None`` if the shape doesn't match."""
295+
for arg in getattr(error, "args", ()):
296+
parsed = arg
297+
if isinstance(parsed, str):
298+
try:
299+
parsed = ast.literal_eval(parsed)
300+
except (ValueError, SyntaxError, MemoryError, RecursionError, TypeError):
301+
continue
302+
if not isinstance(parsed, dict):
303+
continue
304+
inner = parsed.get("error", parsed)
305+
if not isinstance(inner, dict):
306+
continue
307+
data = inner.get("data")
308+
if isinstance(data, str) and data.startswith("Custom error:"):
309+
try:
310+
return int(data.split(":", 1)[1].strip())
311+
except ValueError:
312+
continue
313+
return None
314+
315+
316+
def chain_error_from_substrate_exception(
317+
error: SubstrateRequestException,
318+
) -> Optional["ChainError"]:
319+
"""Translate a transaction-pool validation error into a typed
320+
:class:`ChainError`. Returns ``None`` when the exception doesn't carry a
321+
recognised ``Custom error: N`` code from subtensor's
322+
``CustomTransactionError`` enum, so callers can keep the original."""
323+
if isinstance(error, ChainError):
324+
return None
325+
code = _extract_custom_error_code(error)
326+
if code is None:
327+
return None
328+
exc_cls = _CUSTOM_ERROR_CODE_TO_EXCEPTION.get(code)
329+
if exc_cls is None:
330+
return None
331+
return exc_cls(*error.args)
332+
333+
281334
def map_shield_error(raw_message: str) -> str:
282335
"""Map a raw shield validation error to a human-readable description.
283336

bittensor/core/extrinsics/asyncex/coldkey_swap.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,13 @@ async def swap_coldkey_announced_extrinsic(
392392
)
393393

394394
if response.success:
395+
# The swap may reap the old coldkey's account on chain (resetting its
396+
# nonce). Drop the in-process nonce cache entry so the next extrinsic
397+
# signed by this ss58 re-fetches from the chain instead of incrementing
398+
# a stale value.
399+
subtensor.substrate.clear_nonce_cache_for_account(
400+
wallet.coldkeypub.ss58_address
401+
)
395402
logging.debug("[green]Coldkey swap executed successfully.[/green]")
396403
else:
397404
logging.error(f"[red]{response.message}[/red]")

bittensor/core/extrinsics/asyncex/sudo.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ async def swap_coldkey_extrinsic(
9393
Notes:
9494
- This function can only called by root.
9595
"""
96-
return await sudo_call_extrinsic(
96+
response = await sudo_call_extrinsic(
9797
subtensor=subtensor,
9898
wallet=wallet,
9999
call_module="SubtensorModule",
@@ -108,6 +108,13 @@ async def swap_coldkey_extrinsic(
108108
wait_for_inclusion=wait_for_inclusion,
109109
wait_for_finalization=wait_for_finalization,
110110
)
111+
if response.success:
112+
# The swap may reap the old coldkey's account on chain (resetting its
113+
# nonce). Drop the in-process nonce cache entry so the next extrinsic
114+
# signed by this ss58 re-fetches from the chain instead of incrementing
115+
# a stale value.
116+
subtensor.substrate.clear_nonce_cache_for_account(old_coldkey_ss58)
117+
return response
111118

112119

113120
async def sudo_set_admin_freeze_window_extrinsic(

bittensor/core/settings.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,3 +166,13 @@ class wallet:
166166
e * (_version_int_base**i) for i, e in enumerate(reversed(_version_info))
167167
)
168168
assert version_as_int < 2**31 # fits in int32
169+
170+
171+
# used for backwards compatibility in config.py and loggingmachine.py
172+
def no_parse_cli():
173+
return not os.getenv("BT_NO_PARSE_CLI_ARGS", "true").lower() in (
174+
"0",
175+
"false",
176+
"no",
177+
"off",
178+
)

bittensor/core/subtensor.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
decode_revealed_commitment_with_hotkey,
4949
)
5050
from bittensor.core.config import Config
51-
from bittensor.core.errors import ChainError
51+
from bittensor.core.errors import ChainError, chain_error_from_substrate_exception
5252
from bittensor.core.extrinsics.children import (
5353
root_set_pending_childkey_cooldown_extrinsic,
5454
set_children_extrinsic,
@@ -5012,8 +5012,11 @@ def sign_and_send_extrinsic(
50125012
return extrinsic_response
50135013

50145014
except SubstrateRequestException as error:
5015+
typed = chain_error_from_substrate_exception(error)
5016+
if typed is not None:
5017+
error = typed
50155018
if raise_error:
5016-
raise
5019+
raise error from None
50175020

50185021
extrinsic_response.success = False
50195022
extrinsic_response.message = format_error_message(error)

bittensor/utils/btlogging/loggingmachine.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from statemachine import State, StateMachine
1818

1919
from bittensor.core.config import Config
20-
from bittensor.core.settings import DEFAULTS
20+
from bittensor.core.settings import DEFAULTS, no_parse_cli
2121
from bittensor.utils.btlogging.console import BittensorConsole
2222
from .defines import (
2323
BITTENSOR_LOGGER_NAME,
@@ -686,6 +686,8 @@ def config(cls) -> "Config":
686686
Return:
687687
Configuration object with settings from command-line arguments.
688688
"""
689+
if no_parse_cli():
690+
return Config()
689691
parser = argparse.ArgumentParser()
690692
cls.add_args(parser)
691693
return Config(parser)

0 commit comments

Comments
 (0)