Skip to content
Open
11 changes: 8 additions & 3 deletions bittensor/core/axon.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
from bittensor.core.stream import StreamingSynapse
from bittensor.core.synapse import Synapse, TerminalInfo
from bittensor.core.threadpool import PriorityThreadPoolExecutor
from bittensor.utils import networking
from bittensor.utils import networking, Certificate
from bittensor.utils.axon_utils import allowed_nonce_window_ns, calculate_diff_seconds
from bittensor.utils.btlogging import logging

Expand Down Expand Up @@ -807,7 +807,12 @@ def stop(self) -> "Axon":
self.started = False
return self

def serve(self, netuid: int, subtensor: Optional["Subtensor"] = None) -> "Axon":
def serve(
self,
netuid: int,
subtensor: Optional["Subtensor"] = None,
certificate: Optional[Certificate] = None,
) -> "Axon":
"""
Serves the Axon on the specified subtensor connection using the configured wallet. This method
registers the Axon with a specific subnet within the Bittensor network, identified by the ``netuid``.
Expand All @@ -832,7 +837,7 @@ def serve(self, netuid: int, subtensor: Optional["Subtensor"] = None) -> "Axon":
to start receiving and processing requests from other neurons.
"""
if subtensor is not None and hasattr(subtensor, "serve_axon"):
subtensor.serve_axon(netuid=netuid, axon=self)
subtensor.serve_axon(netuid=netuid, axon=self, certificate=certificate)
return self

async def default_verify(self, synapse: "Synapse"):
Expand Down
1 change: 1 addition & 0 deletions bittensor/core/chain_data/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from .ip_info import IPInfo
from .neuron_info import NeuronInfo
from .neuron_info_lite import NeuronInfoLite
from .neuron_certificate import NeuronCertificate
from .prometheus_info import PrometheusInfo
from .proposal_vote_data import ProposalVoteData
from .scheduled_coldkey_swap_info import ScheduledColdkeySwapInfo
Expand Down
20 changes: 20 additions & 0 deletions bittensor/core/chain_data/neuron_certificate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from dataclasses import dataclass
from typing import List

from bittensor.core.chain_data.utils import from_scale_encoding, ChainDataType
from bittensor.utils import Certificate


# Dataclasses for chain data.
@dataclass
class NeuronCertificate:
"""
Dataclass for neuron certificate.
"""

certificate: Certificate

@classmethod
def from_vec_u8(cls, vec_u8: List[int]) -> "NeuronCertificate":
"""Returns a NeuronCertificate object from a ``vec_u8``."""
return from_scale_encoding(vec_u8, ChainDataType.NeuronCertificate)
7 changes: 7 additions & 0 deletions bittensor/core/chain_data/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ class ChainDataType(Enum):
SubnetHyperparameters = 8
ScheduledColdkeySwapInfo = 9
AccountId = 10
NeuronCertificate = 11


def from_scale_encoding(
Expand Down Expand Up @@ -178,6 +179,12 @@ def from_scale_encoding_using_type_string(
["pruning_score", "Compact<u16>"],
],
},
"NeuronCertificate": {
"type": "struct",
"type_mapping": [
["certificate", "Vec<u8>"],
],
},
"axon_info": {
"type": "struct",
"type_mapping": [
Expand Down
19 changes: 17 additions & 2 deletions bittensor/core/extrinsics/serving.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,12 @@
from bittensor.core.errors import MetadataError
from bittensor.core.extrinsics.utils import submit_extrinsic
from bittensor.core.settings import version_as_int
from bittensor.utils import format_error_message, networking as net, unlock_key
from bittensor.utils import (
format_error_message,
networking as net,
unlock_key,
Certificate,
)
from bittensor.utils.btlogging import logging
from bittensor.utils.networking import ensure_connected

Expand Down Expand Up @@ -57,9 +62,15 @@ def do_serve_axon(
This function is crucial for initializing and announcing a neuron's ``Axon`` service on the network, enhancing the decentralized computation capabilities of Bittensor.
"""

if call_params["certificate"] is None:
del call_params["certificate"]
call_function = "serve_axon"
else:
call_function = "serve_axon_tls"

call = self.substrate.compose_call(
call_module="SubtensorModule",
call_function="serve_axon",
call_function=call_function,
call_params=call_params,
)
extrinsic = self.substrate.create_signed_extrinsic(call=call, keypair=wallet.hotkey)
Expand Down Expand Up @@ -90,6 +101,7 @@ def serve_extrinsic(
placeholder2: int = 0,
wait_for_inclusion: bool = False,
wait_for_finalization=True,
certificate: Optional[Certificate] = None,
) -> bool:
"""Subscribes a Bittensor endpoint to the subtensor chain.

Expand Down Expand Up @@ -124,6 +136,7 @@ def serve_extrinsic(
"protocol": protocol,
"placeholder1": placeholder1,
"placeholder2": placeholder2,
"certificate": certificate,
}
logging.debug("Checking axon ...")
neuron = subtensor.get_neuron_for_pubkey_and_subnet(
Expand Down Expand Up @@ -182,6 +195,7 @@ def serve_axon_extrinsic(
axon: "Axon",
wait_for_inclusion: bool = False,
wait_for_finalization: bool = True,
certificate: Optional[Certificate] = None,
) -> bool:
"""Serves the axon to the network.

Expand Down Expand Up @@ -224,6 +238,7 @@ def serve_axon_extrinsic(
protocol=4,
wait_for_inclusion=wait_for_inclusion,
wait_for_finalization=wait_for_finalization,
certificate=certificate,
)
return serve_success

Expand Down
39 changes: 38 additions & 1 deletion bittensor/core/subtensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
ss58_to_vec_u8,
u16_normalized_float,
hex_to_bytes,
Certificate,
)
from bittensor.utils.balance import Balance
from bittensor.utils.btlogging import logging
Expand Down Expand Up @@ -992,6 +993,7 @@ def serve_axon(
axon: "Axon",
wait_for_inclusion: bool = False,
wait_for_finalization: bool = True,
certificate: Optional[Certificate] = None,
) -> bool:
"""
Registers an ``Axon`` serving endpoint on the Bittensor network for a specific neuron. This function is used to set up the Axon, a key component of a neuron that handles incoming queries and data processing tasks.
Expand All @@ -1008,7 +1010,7 @@ def serve_axon(
By registering an Axon, the neuron becomes an active part of the network's distributed computing infrastructure, contributing to the collective intelligence of Bittensor.
"""
return serve_axon_extrinsic(
self, netuid, axon, wait_for_inclusion, wait_for_finalization
self, netuid, axon, wait_for_inclusion, wait_for_finalization, certificate
)

# metagraph
Expand Down Expand Up @@ -1149,6 +1151,41 @@ def get_neuron_for_pubkey_and_subnet(
block=block,
)

def get_neuron_certificate(
self, hotkey: str, netuid: int, block: Optional[int] = None
) -> Optional["Certificate"]:
"""
Retrieves the TLS certificate for a specific neuron identified by its unique identifier (UID)
within a specified subnet (netuid) of the Bittensor network.

Args:
hotkey (str): The hotkey to query.
netuid (int): The unique identifier of the subnet.
block (Optional[int], optional): The blockchain block number for the query.

Returns:
Optional[Certificate]: the certificate of the neuron if found, ``None`` otherwise.

This function is used for certificate discovery for setting up mutual tls communication between neurons
"""

certificate = self.query_module(
module="SubtensorModule",
name="NeuronCertificates",
block=block,
params=[netuid, hotkey],
)
try:
serialized_certificate = certificate.serialize()
if serialized_certificate:
return (
chr(serialized_certificate["algorithm"])
+ serialized_certificate["public_key"]
)
except AttributeError:
return None
return None

@networking.ensure_connected
def neuron_for_uid(
self, uid: Optional[int], netuid: int, block: Optional[int] = None
Expand Down
4 changes: 3 additions & 1 deletion bittensor/core/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.

from typing import TypedDict
from typing import TypedDict, Optional
from bittensor.utils import Certificate


class AxonServeCallParams(TypedDict):
Expand All @@ -26,6 +27,7 @@ class AxonServeCallParams(TypedDict):
port: int
ip_type: int
netuid: int
certificate: Optional[Certificate]


class PrometheusServeCallParams(TypedDict):
Expand Down
2 changes: 2 additions & 0 deletions bittensor/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@
U16_MAX = 65535
U64_MAX = 18446744073709551615

Certificate = str
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can make a new type that inherits from str but is not str, so that mypy or something will be able to tell the difference between str and Certificate(str)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't it be bytes?



UnlockStatus = namedtuple("UnlockStatus", ["success", "message"])

Expand Down
21 changes: 1 addition & 20 deletions bittensor/utils/mock/subtensor_mock.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
PrometheusInfo,
AxonInfo,
)
from bittensor.core.types import AxonServeCallParams, PrometheusServeCallParams
from bittensor.core.errors import ChainQueryError
from bittensor.core.subtensor import Subtensor
from bittensor.utils import RAOPERTAO, u16_normalized_float
Expand All @@ -39,26 +40,6 @@
__GLOBAL_MOCK_STATE__ = {}


class AxonServeCallParams(TypedDict):
"""Axon serve chain call parameters."""

version: int
ip: int
port: int
ip_type: int
netuid: int


class PrometheusServeCallParams(TypedDict):
"""Prometheus serve chain call parameters."""

version: int
ip: int
port: int
ip_type: int
netuid: int


BlockNumber = int


Expand Down
60 changes: 60 additions & 0 deletions tests/e2e_tests/test_neuron_certificate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import pytest
from bittensor.core.subtensor import Subtensor
from bittensor.core.axon import Axon
from bittensor.utils.btlogging import logging
from tests.e2e_tests.utils.chain_interactions import (
wait_interval,
register_subnet,
)
from tests.e2e_tests.utils.e2e_test_utils import (
setup_wallet,
)


@pytest.mark.asyncio
async def test_neuron_certificate(local_chain):
"""
Tests the metagraph

Steps:
1. Register a subnet through Alice
2. Serve Alice axon with neuron certificate
3. Verify neuron certificate can be retrieved
Raises:
AssertionError: If any of the checks or verifications fail
"""
logging.info("Testing neuron_certificate")
netuid = 1

# Register root as Alice - the subnet owner and validator
alice_keypair, alice_wallet = setup_wallet("//Alice")
register_subnet(local_chain, alice_wallet)

# Verify subnet <netuid> created successfully
assert local_chain.query(
"SubtensorModule", "NetworksAdded", [netuid]
).serialize(), "Subnet wasn't created successfully"

subtensor = Subtensor(network="ws://localhost:9945")

# Register Alice as a neuron on the subnet
assert subtensor.burned_register(
alice_wallet, netuid
), "Unable to register Alice as a neuron"

# Serve Alice's axon with a certificate
axon = Axon(wallet=alice_wallet)
encoded_certificate = "?FAKE_ALICE_CERT"
axon.serve(netuid=netuid, subtensor=subtensor, certificate=encoded_certificate)

await wait_interval(tempo=1, subtensor=subtensor, netuid=netuid)

# Verify we are getting the correct certificate
assert (
subtensor.get_neuron_certificate(
netuid=netuid, hotkey=alice_keypair.ss58_address
)
== encoded_certificate
)

logging.info("✅ Passed test_neuron_certificate")
Loading