Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: Nonce implementation improved #1976

Merged
merged 21 commits into from
Jun 12, 2024
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
ae60736
Feat: Nonce implementation improved
ibraheem-opentensor Jun 4, 2024
e26be77
Removed dupe check
ibraheem-opentensor Jun 4, 2024
2006208
Removed unused func
ibraheem-opentensor Jun 4, 2024
6f92b35
feat: adds BittensorNTPClient
gus-opentensor Jun 4, 2024
745af74
Merge branch 'staging' into feat/gus-abe/nonce-implementation
gus-opentensor Jun 4, 2024
da883e1
Added constants and retries
ibraheem-opentensor Jun 5, 2024
460a982
Nonces: cleanup and fix logging levels
ibraheem-opentensor Jun 5, 2024
f633f9d
Added ntplib in requirements
ibraheem-opentensor Jun 5, 2024
45dc30c
Fixes ruff checks
ibraheem-opentensor Jun 5, 2024
d08d20f
Merge branch 'staging' into feat/gus-abe/nonce-implementation
gus-opentensor Jun 5, 2024
4d82b51
Implementing improvements
ibraheem-opentensor Jun 5, 2024
b412b80
Ruff: Updated format
ibraheem-opentensor Jun 5, 2024
67c4721
Fix linter and type errors
ibraheem-opentensor Jun 6, 2024
a836be0
Replace NTP with system time
ibraheem-opentensor Jun 6, 2024
9e7efa0
Updated constants
ibraheem-opentensor Jun 6, 2024
ce4d535
Merge branch 'staging' into feat/gus-abe/nonce-implementation
ibraheem-opentensor Jun 6, 2024
8ef6cfe
Cleanup
ibraheem-opentensor Jun 6, 2024
125c561
Merge branch 'staging' into feat/gus-abe/nonce-implementation
ibraheem-opentensor Jun 11, 2024
ba0f02c
Merge branch 'staging' into feat/gus-abe/nonce-implementation
ibraheem-opentensor Jun 11, 2024
dc6daf8
Nonce: Included timeout and fixed versioning
ibraheem-opentensor Jun 11, 2024
09ae1da
Merge branch 'staging' into feat/gus-abe/nonce-implementation
ibraheem-opentensor Jun 12, 2024
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
48 changes: 32 additions & 16 deletions bittensor/axon.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,10 @@
PostProcessException,
SynapseException,
)
from bittensor.constants import ALLOWED_DELTA, V_7_2_0
from bittensor.threadpool import PriorityThreadPoolExecutor
from bittensor.utils import networking
from bittensor.utils.networking import BittensorNTPClient


class FastAPIThreadedServer(uvicorn.Server):
Expand Down Expand Up @@ -342,12 +344,12 @@ def __init__(
self.port = self.config.axon.port
self.external_ip = (
self.config.axon.external_ip
if self.config.axon.external_ip != None
if self.config.axon.external_ip is not None
else bittensor.utils.networking.get_external_ip()
)
self.external_port = (
self.config.axon.external_port
if self.config.axon.external_port != None
if self.config.axon.external_port is not None
else self.config.axon.port
)
self.full_address = str(self.config.axon.ip) + ":" + str(self.config.axon.port)
Expand Down Expand Up @@ -888,25 +890,38 @@ async def default_verify(self, synapse: bittensor.Synapse):
# Build the unique endpoint key.
endpoint_key = f"{synapse.dendrite.hotkey}:{synapse.dendrite.uuid}"

# Check the nonce from the endpoint key with 4 second delta
allowedDelta = 4000000000

# Requests must have nonces to be safe from replays
if synapse.dendrite.nonce is None:
raise Exception("Missing Nonce")

# If we don't have a nonce stored, ensure that the nonce falls within
# a reasonable delta.

# Updated nonce using NTP implementated at v7.2
if (
self.nonces.get(endpoint_key) is None
and synapse.dendrite.nonce <= time.time_ns() - allowedDelta
):
raise Exception("Nonce is too old")
if (
self.nonces.get(endpoint_key) is not None
and synapse.dendrite.nonce <= self.nonces[endpoint_key]
synapse.dendrite.version is not None
and synapse.dendrite.version >= V_7_2_0
):
raise Exception("Nonce is too old")
# If we don't have a nonce stored, ensure that the nonce falls within
# a reasonable delta.
current_time = BittensorNTPClient.get_current_ntp_time()
ibraheem-opentensor marked this conversation as resolved.
Show resolved Hide resolved
if (
self.nonces.get(endpoint_key) is None
and synapse.dendrite.nonce <= current_time - ALLOWED_DELTA
ibraheem-opentensor marked this conversation as resolved.
Show resolved Hide resolved
):
raise Exception("Nonce is too old")
if (
self.nonces.get(endpoint_key) is not None
and synapse.dendrite.nonce <= self.nonces[endpoint_key]
):
raise Exception("Nonce is too old")
else:
if (
endpoint_key in self.nonces.keys()
and self.nonces[endpoint_key] is not None
and synapse.dendrite.nonce <= self.nonces[endpoint_key]
ibraheem-opentensor marked this conversation as resolved.
Show resolved Hide resolved
):
raise Exception("Nonce is too small")

if not keypair.verify(message, synapse.dendrite.signature):
raise Exception(
Expand Down Expand Up @@ -1149,7 +1164,7 @@ async def preprocess(self, request: Request) -> bittensor.Synapse:
# Extracts the request name from the URL path.
try:
request_name = request.url.path.split("/")[1]
except:
except Exception:
raise InvalidRequestNameError(
f"Improperly formatted request. Could not parser request {request.url.path}."
)
Expand All @@ -1164,18 +1179,19 @@ async def preprocess(self, request: Request) -> bittensor.Synapse:

try:
synapse = request_synapse.from_headers(request.headers) # type: ignore
except Exception as e:
except Exception:
raise SynapseParsingError(
f"Improperly formatted request. Could not parse headers {request.headers} into synapse of type {request_name}."
)
synapse.name = request_name
current_time = BittensorNTPClient.get_current_ntp_time()

# Fills the local axon information into the synapse.
synapse.axon.__dict__.update(
{
"version": str(bittensor.__version_as_int__),
"uuid": str(self.axon.uuid),
"nonce": f"{time.time_ns()}",
"nonce": current_time,
"status_code": 100,
}
)
Expand Down
22 changes: 22 additions & 0 deletions bittensor/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# The MIT License (MIT)
# Copyright © 2023 OpenTensor Foundation

# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
# documentation files (the “Software”), to deal in the Software without restriction, including without limitation
# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

# The above copyright notice and this permission notice shall be included in all copies or substantial portions of
# the Software.

# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO
# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.


ALLOWED_DELTA = 5000000000 # Delta of 5 seconds for nonce validation
NTP_POOL_RETRIES = 2
NTP_SERVERS = ["0.pool.ntp.org", "1.pool.ntp.org", "2.pool.ntp.org"]
V_7_2_0 = 720
ibraheem-opentensor marked this conversation as resolved.
Show resolved Hide resolved
14 changes: 9 additions & 5 deletions bittensor/dendrite.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@
import uuid
import time
import aiohttp

import bittensor
from typing import Optional, List, Union, AsyncGenerator, Any
from bittensor.utils.networking import BittensorNTPClient
from bittensor.utils.registration import torch, use_torch


Expand Down Expand Up @@ -311,7 +313,7 @@ def query(
try:
loop = asyncio.get_event_loop()
result = loop.run_until_complete(self.forward(*args, **kwargs))
except:
except Exception:
new_loop = asyncio.new_event_loop()
asyncio.set_event_loop(new_loop)
result = loop.run_until_complete(self.forward(*args, **kwargs))
Expand Down Expand Up @@ -653,12 +655,11 @@ def preprocess_synapse_for_request(
"""
# Set the timeout for the synapse
synapse.timeout = timeout

# Build the Dendrite headers using the local system's details
current_time = BittensorNTPClient.get_current_ntp_time()
synapse.dendrite = bittensor.TerminalInfo(
ip=self.external_ip,
version=bittensor.__version_as_int__,
nonce=time.time_ns(),
nonce=current_time,
uuid=self.uuid,
hotkey=self.keypair.ss58_address,
)
Expand Down Expand Up @@ -705,7 +706,10 @@ def process_server_response(
# Set the attribute in the local synapse from the corresponding
# attribute in the server synapse
setattr(local_synapse, key, getattr(server_synapse, key))
except:
except Exception as e:
bittensor.logging.info(
f"Ignoring error when setting attribute: {e}"
)
# Ignore errors during attribute setting
pass
else:
Expand Down
37 changes: 37 additions & 0 deletions bittensor/utils/networking.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,17 @@
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.

# Standard Lib
import bittensor
import os
import urllib
import time
import json
import netaddr
from bittensor.constants import NTP_POOL_RETRIES, NTP_SERVERS

# 3rd party
import ntplib
import requests


Expand Down Expand Up @@ -168,3 +175,33 @@ def get_formatted_ws_endpoint_url(endpoint_url: str) -> str:
endpoint_url = "ws://{}".format(endpoint_url)

return endpoint_url


class BittensorNTPClient:
"""NTP singleton client"""

_instance = None

def __new__(cls):
if cls._instance is None:
cls._instance = ntplib.NTPClient()
return cls._instance

@staticmethod
def get_current_ntp_time(retries: int = NTP_POOL_RETRIES) -> int:
attempts = 0
while attempts < retries:
server = NTP_SERVERS[attempts % len(NTP_SERVERS)]
try:
ntp_client = BittensorNTPClient()
response = ntp_client.request(server)
current_time = int(response.tx_time * 1e9) # Convert to nanoseconds
return current_time
except Exception as e:
attempts += 1
bittensor.logging.info(
f"Attempt {attempts} - Error fetching NTP time: {e}"
)
# Fallback to local time if all retries fail
bittensor.logging.warning("All NTP retries failed, using system UNIX time")
return time.time_ns()
1 change: 1 addition & 0 deletions requirements/prod.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ fuzzywuzzy>=0.18.0
fastapi~=0.110.1
munch~=2.5.0
netaddr
ntplib==0.4.0
numpy
msgpack-numpy-opentensor~=0.5.0
nest_asyncio
Expand Down
7 changes: 7 additions & 0 deletions tests/unit_tests/utils/test_networking.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,3 +165,10 @@ def urlopen(self):
def test_format(url: str, expected: str):
"""Test formatting WebSocket endpoint URL."""
assert utils.networking.get_formatted_ws_endpoint_url(url) == expected


def test_bt_ntp_client():
client_1 = utils.networking.BittensorNTPClient()
client_2 = utils.networking.BittensorNTPClient()

assert client_1 == client_2
Loading