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 4 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
81 changes: 80 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,84 @@ 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
async def test_default_verify_empty_signature_raises():
# Regression: an empty signature must NOT skip verification. Previously the
# check was `if signature and not verify(...)`, so an empty signature
# short-circuited and the request was accepted, allowing hotkey spoofing.
axon, synapse, _ = _make_default_verify_inputs()
synapse.dendrite.signature = ""

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


@pytest.mark.asyncio
async def test_default_verify_missing_signature_raises():
axon, synapse, _ = _make_default_verify_inputs()
synapse.dendrite.signature = None

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
Loading