Skip to content
This repository was archived by the owner on Jul 10, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 4 additions & 3 deletions bittensor/core/axon.py
Original file line number Diff line number Diff line change
Expand Up @@ -974,9 +974,10 @@ async def default_verify(self, synapse: "Synapse"):
):
raise Exception("Nonce is too old, a newer one was last processed")

if synapse.dendrite.signature and not keypair.verify(
message, synapse.dendrite.signature
):
if not synapse.dendrite.signature:
raise Exception("Missing Signature")

if not keypair.verify(message, synapse.dendrite.signature):
raise Exception(
f"Signature mismatch with {message} and {synapse.dendrite.signature}"
)
Expand Down
73 changes: 72 additions & 1 deletion tests/unit_tests/test_axon.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,15 @@
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.synapse import Synapse, TerminalInfo
from bittensor.core.threadpool import PriorityThreadPoolExecutor
from bittensor.utils.axon_utils import (
allowed_nonce_window_ns,
calculate_diff_seconds,
ALLOWED_DELTA,
NANOSECONDS_IN_SECOND,
)
from bittensor_wallet import Keypair


def test_attach_initial(mock_get_external_ip):
Expand Down Expand Up @@ -755,6 +756,76 @@ def test_nonce_diff_seconds(nonce_offset_seconds):
)


# ---------------------------------------------------------------------------
# default_verify signature checks
# ---------------------------------------------------------------------------
def _make_default_verify_inputs():
"""Build an (axon, synapse, dendrite_keypair) trio for default_verify.

The synapse carries a fresh nonce (so the v7.2 freshness check passes) and
a valid dendrite hotkey; each test sets `synapse.dendrite.signature` to
probe the signature branch.
"""
dendrite_keypair = Keypair.create_from_uri("//Alice")
receiver_keypair = Keypair.create_from_uri("//Bob")
axon = Axon(
ip="192.0.2.1",
external_ip="192.0.2.1",
wallet=MockWallet(MockHotkey(receiver_keypair.ss58_address)),
)
synapse = SynapseMock()
synapse.dendrite = TerminalInfo(
hotkey=dendrite_keypair.ss58_address,
nonce=time.time_ns(),
uuid="5ecbd69c-1cec-11ee-b0dc-e29ce36fec1a",
version=version_as_int,
)
return axon, synapse, dendrite_keypair


def _default_verify_message(axon, synapse):
return (
f"{synapse.dendrite.nonce}.{synapse.dendrite.hotkey}."
f"{axon.wallet.hotkey.ss58_address}.{synapse.dendrite.uuid}."
f"{synapse.computed_body_hash}"
)


@pytest.mark.asyncio
async def test_default_verify_valid_signature_passes():
axon, synapse, dendrite_keypair = _make_default_verify_inputs()
message = _default_verify_message(axon, synapse)
synapse.dendrite.signature = "0x" + dendrite_keypair.sign(message).hex()

await axon.default_verify(synapse)

endpoint_key = f"{synapse.dendrite.hotkey}:{synapse.dendrite.uuid}"
assert axon.nonces[endpoint_key] == synapse.dendrite.nonce


@pytest.mark.asyncio
@pytest.mark.parametrize("empty_sig", ["", None])
async def test_default_verify_missing_signature_raises(empty_sig):
# Regression: a falsy signature must NOT skip verification. Previously the
# check was `if signature and not verify(...)`, so an empty/None signature
# short-circuited and the request was accepted, allowing hotkey spoofing.
axon, synapse, _ = _make_default_verify_inputs()
synapse.dendrite.signature = empty_sig

with pytest.raises(Exception, match="Missing Signature"):
await axon.default_verify(synapse)


@pytest.mark.asyncio
async def test_default_verify_wrong_signature_raises():
# A present-but-invalid signature is still rejected (unchanged behavior).
axon, synapse, _ = _make_default_verify_inputs()
synapse.dendrite.signature = "0x" + ("00" * 64)

with pytest.raises(Exception, match="Signature mismatch"):
await axon.default_verify(synapse)


# 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):
Expand Down