diff --git a/bittensor/core/axon.py b/bittensor/core/axon.py index 3fd9dd46c2..61573ae6fc 100644 --- a/bittensor/core/axon.py +++ b/bittensor/core/axon.py @@ -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}" ) diff --git a/tests/unit_tests/test_axon.py b/tests/unit_tests/test_axon.py index d1b8e03a97..0712e50dc1 100644 --- a/tests/unit_tests/test_axon.py +++ b/tests/unit_tests/test_axon.py @@ -21,7 +21,7 @@ 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, @@ -29,6 +29,7 @@ ALLOWED_DELTA, NANOSECONDS_IN_SECOND, ) +from bittensor_wallet import Keypair def test_attach_initial(mock_get_external_ip): @@ -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):