From b7cac6f56d8e75231669bcbbeef3f4343b5fd5e5 Mon Sep 17 00:00:00 2001 From: eclipsevortex Date: Fri, 24 May 2024 14:54:52 +0100 Subject: [PATCH 1/6] resyn metagraph for miner --- neurons/miner.py | 18 ++++++++++++++++++ subnet/miner/run.py | 14 ++++++++++---- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/neurons/miner.py b/neurons/miner.py index 86f12b16..62fc4638 100644 --- a/neurons/miner.py +++ b/neurons/miner.py @@ -176,6 +176,7 @@ def __init__(self): self.thread: threading.Thread = None self.lock = asyncio.Lock() self.request_timestamps: typing.Dict = {} + self.previous_last_updates = [] self.step = 0 @@ -263,6 +264,23 @@ def __exit__(self, exc_type, exc_value, traceback): """ self.stop_run_thread() + def should_sync_metagraph(self): + """ + True if the metagraph has been resynced, False otherwise. + """ + last_updates = self.subtensor.substrate.query( + module="SubtensorModule", + storage_function="LastUpdate", + params=[self.config.netuid], + ) + if self.previous_last_updates == last_updates: + return False + + # Save the new updates + self.previous_last_updates = last_updates + + return True + def run_miner(): """ diff --git a/subnet/miner/run.py b/subnet/miner/run.py index ac3f1d5a..ca34336e 100644 --- a/subnet/miner/run.py +++ b/subnet/miner/run.py @@ -57,6 +57,16 @@ def run(self): def handler(obj, update_nr, subscription_id): current_block = obj["header"]["number"] bt.logging.debug(f"New block #{current_block}") + bt.logging.debug( + f"Blocks since epoch: {(current_block + netuid + 1) % (tempo + 1)}" + ) + + # --- Check to resync the metagraph. + should_sync = self.should_sync_metagraph() + bt.logging.debug(f"should_sync_metagraph() {should_sync}") + if should_sync: + self.metagraph.sync(subtensor=self.subtensor) + bt.logging.info("Metagraph resynced") # --- Check for registration every 100 blocks (20 minutes). if current_block % 100 == 0: @@ -71,10 +81,6 @@ def handler(obj, update_nr, subscription_id): self.last_upgrade_check = time.time() - bt.logging.debug( - f"Blocks since epoch: {(current_block + netuid + 1) % (tempo + 1)}" - ) - if self.should_exit: return True From 65a1987a31d9ed30e6e44b1ad58eebd39fde77a0 Mon Sep 17 00:00:00 2001 From: eclipsevortex Date: Tue, 21 May 2024 17:23:14 +0100 Subject: [PATCH 2/6] fix country api --- neurons/validator.py | 48 ++++-- subnet/country/country.py | 162 ++++++++++++++++++ subnet/country/country_constants.py | 9 + subnet/validator/challenge.py | 9 +- subnet/validator/localisation.py | 48 ++---- subnet/validator/miner.py | 40 +++-- subnet/validator/score.py | 27 +-- subnet/validator/state.py | 6 +- tests/unit_tests/conftest.py | 1 + .../subnet/validator/test_latency_score.py | 28 +-- .../subnet/validator/test_resync_miners.py | 62 +++++-- 11 files changed, 329 insertions(+), 111 deletions(-) create mode 100644 subnet/country/country.py create mode 100644 subnet/country/country_constants.py diff --git a/neurons/validator.py b/neurons/validator.py index 6365f480..e191c848 100644 --- a/neurons/validator.py +++ b/neurons/validator.py @@ -28,6 +28,7 @@ from subnet import __version__ as THIS_VERSION from subnet.monitor.monitor import Monitor +from subnet.country.country import CountryService from subnet.shared.checks import check_registration from subnet.shared.utils import get_redis_password, should_upgrade @@ -36,7 +37,6 @@ from subnet.shared.mock import MockMetagraph, MockDendrite, MockSubtensor from subnet.validator.config import config, check_config, add_args -from subnet.validator.localisation import get_country, get_localisation from subnet.validator.forward import forward from subnet.validator.models import Miner from subnet.validator.version import VersionControl @@ -168,19 +168,6 @@ def __init__(self, config=None): self.dendrite = bt.dendrite(wallet=self.wallet) bt.logging.debug(str(self.dendrite)) - # Get the validator country - self.country = get_country(self.dendrite.external_ip) - country_localisation = get_localisation(self.country) - country_name = ( - country_localisation["country"] if country_localisation else "None" - ) - bt.logging.debug(f"Validator based in {country_name}") - - # Init wandb. - if not self.config.wandb.off: - bt.logging.debug("loading wandb") - init_wandb(self) - # Init the event loop. self.loop = asyncio.get_event_loop() @@ -195,6 +182,7 @@ def __init__(self, config=None): self.rebalance_queue = [] self.miners: List[Miner] = [] self.last_upgrade_check = 0 + self.last_upgrade_country = None async def run(self): bt.logging.info("run()") @@ -203,6 +191,24 @@ async def run(self): dump_path = self.config.database.redis_dump_path self.version_control = VersionControl(self.database, dump_path) + # Monitor miners + self.monitor = Monitor(self.config.netuid) + self.monitor.start() + + # Country service + self.country_service = CountryService(self.config.netuid) + self.country_service.start() + self.country_service.wait() + + # Get the validator country + self.country_code = self.country_service.get_country(self.dendrite.external_ip) + bt.logging.debug(f"Validator based in {self.country_code}") + + # Init wandb. + if not self.config.wandb.off: + bt.logging.debug("loading wandb") + init_wandb(self) + # Init miners self.miners = await get_all_miners(self) bt.logging.debug(f"Miners loaded {len(self.miners)}") @@ -210,10 +216,6 @@ async def run(self): # Load the state load_state(self) - # Monitor miners - self.monitor = Monitor(self.config.netuid) - self.monitor.start() - try: while 1: # Start the upgrade process every 10 minutes @@ -229,7 +231,12 @@ async def run(self): start_epoch = time.time() - await resync_metagraph_and_miners(self) + # Check if the coutry changed + last_upgrade_country = self.country_service.get_last_modified() + has_country_changed = self.last_upgrade_country != last_upgrade_country + self.last_upgrade_country = last_upgrade_country + + await resync_metagraph_and_miners(self, has_country_changed) prev_set_weights_block = self.metagraph.last_update[self.uid].item() # --- Wait until next step epoch. @@ -308,6 +315,9 @@ async def run_forward(): if self.monitor: self.monitor.stop() + if self.country_service: + self.country_service.stop() + if hasattr(self, "subtensor"): bt.logging.debug("Closing subtensor connection") self.subtensor.close() diff --git a/subnet/country/country.py b/subnet/country/country.py new file mode 100644 index 00000000..5bb4b056 --- /dev/null +++ b/subnet/country/country.py @@ -0,0 +1,162 @@ +import time +import copy +import requests +import threading +import bittensor as bt +from datetime import datetime +from typing import List + +from subnet.country.country_constants import ( + COUNTRY_URL, + LOGGING_NAME, + COUNTRY_SLEEP, +) +from subnet.validator.localisation import ( + get_country_by_country_is, + get_country_by_ip_api, + get_country_by_ipinfo_io, + get_country_by_my_api, +) + + +class CountryService(threading.Thread): + def __init__(self, netuid: int): + super().__init__() + self.stop_flag = threading.Event() + self._lock = threading.Lock() + self._data = {} + self.netuid = netuid + self.last_modified = None + self.show_not_found = True + self.first_try = True + + # Allow us to not display multiple time the same errors + self.error_message = None + + def _is_custom_api_enabled(self): + with self._lock: + return self._data.get("enable_custom_api") or True + + def get_last_modified(self): + with self._lock: + return self.last_modified + + def get_locations(self) -> List[str]: + with self._lock: + localisations = self._data.get("localisations") or {} + return copy.deepcopy(localisations) + + def get_country(self, ip: str): + """ + Get the country code of the ip + """ + country = None + with self._lock: + overrides = self._data.get("overrides") or {} + country = overrides.get(ip) + + if country: + return country + + country, reason1 = ( + get_country_by_my_api(ip) if self._is_custom_api_enabled() else (None, None) + ) + if country: + return country + + country, reason2 = get_country_by_country_is(ip) + if country: + return country + + country, reason3 = get_country_by_ip_api(ip) + if country: + return country + + country, reason4 = get_country_by_ipinfo_io(ip) + if country: + return country + + bt.logging.warning( + f"Could not get the country of the ip {ip}: Api 1: {reason1} / Api 2: {reason2} / Api 3: {reason3} / Api 4: {reason4}" + ) + return None + + def start(self): + super().start() + bt.logging.debug(f"Country started") + + def stop(self): + self.stop_flag.set() + super().join() + bt.logging.debug(f"Country stopped") + + def wait(self): + """ + Wait until we have execute the run method at least one + """ + attempt = 0 + while self.first_try or attempt > 5: + time.sleep(1) + attempt += 1 + + def run(self): + while not self.stop_flag.is_set(): + response = None + try: + # Sleep before requesting again + if not self.first_try: + time.sleep(COUNTRY_SLEEP) + + url = COUNTRY_URL.get(self.netuid) + if not url: + bt.logging.warning( + f"Could not find the country file for the subnet {self.netuid}" + ) + + response = requests.get(url) + if response.status_code != 200: + if response.status_code == 404 and not self.show_not_found: + continue + + self.show_not_found = response.status_code != 404 + + error_message = f"[{LOGGING_NAME}] Could not get the country file {response.status_code}: {response.reason}" + if error_message != self.error_message: + bt.logging.warning(error_message) + self.error_message = error_message + + continue + + # Load the data + data = response.json() or {} + + # Check is date can be retrieved + remote_last_modified = data.get("last-modified") + if remote_last_modified is None: + continue + + # Check if data changed + last_modified = datetime.strptime( + remote_last_modified, "%Y-%m-%d %H:%M:%S.%f" + ) + if self.last_modified and last_modified <= self.last_modified: + continue + + if self._data == data: + continue + + with self._lock: + self._data = data + self.last_modified = last_modified + + bt.logging.success( + f"[{LOGGING_NAME}] Country file proceed successfully", + ) + except Exception as err: + content = response.content if response else "" + error_message = f"[{LOGGING_NAME}] An error during country file processing: {err} {type(err)} {content}" + if error_message != self.error_message: + bt.logging.error(error_message) + self.error_message = error_message + finally: + self.first_try = False diff --git a/subnet/country/country_constants.py b/subnet/country/country_constants.py new file mode 100644 index 00000000..9a00bebe --- /dev/null +++ b/subnet/country/country_constants.py @@ -0,0 +1,9 @@ +# Logging +LOGGING_NAME = "Country" + +# Monitor +COUNTRY_SLEEP = 5 * 60 # Every 5 minutes +COUNTRY_URL = { + 7: "http://drive.google.com/uc?id=14RkFaEuwfc8lnJghNc3oKLT32kqcdjTd&export=download", + 92: "http://drive.google.com/uc?id=1r1uBSkLnehMsIx9csVcrtr-Iod8ij8ZE&export=download", +} diff --git a/subnet/validator/challenge.py b/subnet/validator/challenge.py index e3f4c74b..062c7a84 100644 --- a/subnet/validator/challenge.py +++ b/subnet/validator/challenge.py @@ -6,7 +6,6 @@ from subnet.constants import DEFAULT_PROCESS_TIME from subnet.shared.subtensor import get_current_block from subnet.validator.models import Miner -from subnet.validator.miner import get_miner_ip_occurences from subnet.validator.synapse import send_scope from subnet.validator.security import is_miner_suspicious from subnet.validator.utils import ( @@ -106,6 +105,9 @@ async def challenge_data(self): suspicious_uids = self.monitor.get_suspicious_uids() bt.logging.debug(f"[{CHALLENGE_NAME}] Suspicious uids {suspicious_uids}") + # Get the locations + locations = self.country_service.get_locations() + # Execute the challenges tasks = [] reasons = [] @@ -126,6 +128,7 @@ async def challenge_data(self): miner: Miner = next((miner for miner in self.miners if miner.uid == uid), None) bt.logging.info(f"[{CHALLENGE_NAME}][{miner.uid}] Computing score...") + bt.logging.debug(f"[{CHALLENGE_NAME}][{miner.uid}] Country {miner.country}") # Check if the miner is suspicious miner.suspicious, miner.penalty_factor = is_miner_suspicious( @@ -151,7 +154,9 @@ async def challenge_data(self): ) # Compute score for latency - miner.latency_score = compute_latency_score(self.country, miner, self.miners) + miner.latency_score = compute_latency_score( + self.country_code, miner, self.miners, locations + ) bt.logging.debug( f"[{CHALLENGE_NAME}][{miner.uid}] Latency score {miner.latency_score}" ) diff --git a/subnet/validator/localisation.py b/subnet/validator/localisation.py index b2fc81b8..954314ba 100644 --- a/subnet/validator/localisation.py +++ b/subnet/validator/localisation.py @@ -1,9 +1,8 @@ -import json -import os import requests import bittensor as bt from math import radians, sin, cos, sqrt, atan2 +MY_API_BASE_URL = "http://api.ip-from.com" COUNTRY_IS_BASE_URL = "https://api.country.is" IP_API_BASE_URL = "http://ip-api.com/json" IPINFO_IO_BASE_URL = "https://ipinfo.io" @@ -11,19 +10,24 @@ countries = {} -def get_localisation(country_code: str): +def get_country_by_my_api(ip: str): """ - Get the longitude and latitude of the country + Get the country code of the ip (use Maxwind and IpInfo) + Reference: http://api.ip-from.com """ - global countries - if len(countries) == 0: - current_dir = os.path.dirname(os.path.realpath(__file__)) - file_path = os.path.join(current_dir, "..", "localisation.json") + url = f"{MY_API_BASE_URL}/{ip}" - with open(file_path, "r") as f: - countries = json.load(f) + response = requests.get(url) + + if response.status_code != 200: + return None, response.reason + + data = response.json() - return countries.get(country_code) + maxmind_country = data.get("maxmind_country") + ipinfo_country = data.get("ipinfo_country") + + return (maxmind_country, None) if maxmind_country else (ipinfo_country, None) def get_country_by_country_is(ip: str): @@ -77,28 +81,6 @@ def get_country_by_ipinfo_io(ip: str): return data.get("country"), None -def get_country(ip: str): - """ - Get the country code of the ip - """ - country, reason1 = get_country_by_country_is(ip) - if country: - return country - - country, reason2 = get_country_by_ip_api(ip) - if country: - return country - - country, reason3 = get_country_by_ipinfo_io(ip) - if country: - return country - - bt.logging.warning( - f"Could not get the country of the ip {ip}: Api 1: {reason1} / Api 2: {reason2} / Api 3: {reason3}" - ) - return None - - def compute_localisation_distance(lat1, lon1, lat2, lon2): """ Compute the distance between two localisations using Haversine formula diff --git a/subnet/validator/miner.py b/subnet/validator/miner.py index bf1dd18f..f85fd33f 100644 --- a/subnet/validator/miner.py +++ b/subnet/validator/miner.py @@ -9,7 +9,6 @@ compute_final_score, ) from subnet.validator.utils import get_available_uids, check_uid_availability -from subnet.validator.localisation import get_country from subnet.validator.database import ( get_hotkey_statistics, remove_hotkey_stastitics, @@ -52,16 +51,16 @@ async def get_all_miners(self) -> List[Miner]: uid=uid, ip=axon.ip, hotkey=axon.hotkey, - country=get_country(axon.ip), + country=self.country_service.get_country(axon.ip), ip_occurences=ip_occurences, ) - await update_hotkey_statistics(axon.hotkey, miner.snapshot, self.database) else: # In hash set everything is stored as a string to the verified need to be manage differently + country = self.country_service.get_country(axon.ip) + if country == None: + country = get_field_value(statistics.get(b"country")) + version = get_field_value(statistics.get(b"version"), "0.0.0") - country = get_field_value(statistics.get(b"country")) or get_country( - axon.ip - ) verified = get_field_value(statistics.get(b"verified"), "0") score = get_field_value(statistics.get(b"score"), 0) availability_score = get_field_value( @@ -98,6 +97,9 @@ async def get_all_miners(self) -> List[Miner]: process_time=process_time, ) + # Update the database just to be sure we have the right country + await update_hotkey_statistics(axon.hotkey, miner.snapshot, self.database) + miners.append(miner) return miners @@ -107,7 +109,9 @@ async def add_new_miner(self, uid: int, ip: str, hotkey: str): """ Add a new miner """ - miner = Miner(uid=uid, ip=ip, hotkey=hotkey, country=get_country(ip)) + miner = Miner( + uid=uid, ip=ip, hotkey=hotkey, country=self.country_service.get_country(ip) + ) self.miners.append(miner) return miner @@ -123,19 +127,19 @@ async def replace_old_miner(self, ip: str, hotkey: str, miner: Miner): await remove_hotkey_stastitics(miner.hotkey, self.database) # Reset the new miner - miner.reset(ip, hotkey, get_country(ip)) + miner.reset(ip, hotkey, self.country_service.get_country(ip)) return old_hotkey -def move_miner(ip: str, miner: Miner): +def move_miner(self, ip: str, miner: Miner): """ Move an existing miner from a host to another one """ previous_ip = miner.ip # Reset the miner as it changed ip so everything has to be re-evaluated - miner.reset(ip, miner.hotkey, get_country(ip)) + miner.reset(ip, miner.hotkey, self.country_service.get_country(ip)) return previous_ip @@ -197,11 +201,20 @@ async def resync_miners(self): # Check the miner has been moved to another VPS if miner.ip != ip: - previous_ip = move_miner(ip, miner) + previous_ip = move_miner(self, ip, miner) bt.logging.success( f"[{miner.uid}] Miner moved from {previous_ip} to {miner.ip}" ) + # Check if the country has been overrided + country_overrided = self.country_service.get_country(miner.ip) + if country_overrided and miner.country != country_overrided: + previous_country = miner.country + miner.country = country_overrided + bt.logging.success( + f"[{miner.uid}][{previous_country}] Miner's country overrided by {miner.country}" + ) + # Focus on impacts resulting of these changes bt.logging.debug("resync_miners() refreshing ip occurences") ips = [miner.ip for miner in self.miners] @@ -210,12 +223,15 @@ async def resync_miners(self): miner.ip_occurences = get_miner_ip_occurences(miner.ip, ips) bt.logging.debug("resync_miners() refreshing scores") + locations = self.country_service.get_locations() for miner in self.miners: # Refresh the availability score miner.availability_score = compute_availability_score(miner) # Refresh latency score - miner.latency_score = compute_latency_score(self.country, miner, self.miners) + miner.latency_score = compute_latency_score( + self.country_code, miner, self.miners, locations + ) # Refresh the distribution score miner.distribution_score = compute_distribution_score(miner, self.miners) diff --git a/subnet/validator/score.py b/subnet/validator/score.py index de7a33dc..063a5de2 100644 --- a/subnet/validator/score.py +++ b/subnet/validator/score.py @@ -4,10 +4,7 @@ from subnet.validator.models import Miner from subnet.validator.bonding import wilson_score_interval -from subnet.validator.localisation import ( - compute_localisation_distance, - get_localisation, -) +from subnet.validator.localisation import compute_localisation_distance from subnet.validator.constants import CHALLENGE_NAME from subnet.constants import ( AVAILABILITY_FAILURE_REWARD, @@ -94,7 +91,9 @@ def can_compute_latency_score(miner: Miner): return miner.verified and not miner.has_ip_conflicts -def compute_latency_score(validator_country: str, miner: Miner, miners: List[Miner]): +def compute_latency_score( + validator_country: str, miner: Miner, miners: List[Miner], locations +): """ Compute the latency score of the uid based on the process time of all uids """ @@ -104,7 +103,7 @@ def compute_latency_score(validator_country: str, miner: Miner, miners: List[Min bt.logging.trace(f"[{miner.uid}][Score][Latency] Process time {miner.process_time}") # Step 1: Get the localisation of the validator - validator_localisation = get_localisation(validator_country) + validator_localisation = locations.get(validator_country) # Step 2: Compute the miners process times by adding a tolerance miner_index = -1 @@ -115,8 +114,8 @@ def compute_latency_score(validator_country: str, miner: Miner, miners: List[Min continue distance = 0 - location = get_localisation(item.country) - if location is not None: + location = locations.get(item.country) + if location is not None and validator_localisation is not None: distance = compute_localisation_distance( validator_localisation["latitude"], validator_localisation["longitude"], @@ -124,9 +123,15 @@ def compute_latency_score(validator_country: str, miner: Miner, miners: List[Min location["longitude"], ) else: - bt.logging.warning( - f"[{miner.uid}][Score][Latency] The country '{item.country}' could not be found. No tolerance applied." - ) + if validator_localisation is None: + bt.logging.warning( + f"[{miner.uid}][Score][Latency] The validator's country '{validator_country}' could not be found. No tolerance applied." + ) + + if location is None: + bt.logging.warning( + f"[{miner.uid}][Score][Latency] The country '{item.country}' could not be found. No tolerance applied." + ) scaled_distance = distance / MAX_DISTANCE tolerance = 1 - scaled_distance diff --git a/subnet/validator/state.py b/subnet/validator/state.py index eac83fdf..b7445630 100644 --- a/subnet/validator/state.py +++ b/subnet/validator/state.py @@ -41,12 +41,12 @@ def should_checkpoint(current_block, prev_step_block, checkpoint_block_length): return current_block - prev_step_block >= checkpoint_block_length -async def resync_metagraph_and_miners(self): +async def resync_metagraph_and_miners(self, force_refresh = False): """Checkpoints the training process.""" bt.logging.info("checkpoint()") resynched = resync_metagraph(self) - if resynched: + if resynched or force_refresh: # Resync miners list await resync_miners(self) @@ -328,7 +328,7 @@ def init_wandb(self): THIS_VERSION, str(THIS_SPEC_VERSION), f"netuid_{self.metagraph.netuid}", - self.country, + self.country_code, ] if self.config.mock: diff --git a/tests/unit_tests/conftest.py b/tests/unit_tests/conftest.py index f89d38c8..a742bb8b 100644 --- a/tests/unit_tests/conftest.py +++ b/tests/unit_tests/conftest.py @@ -13,6 +13,7 @@ def validator(): config.wandb.off = True config.neuron.dont_save_events = True validator = Validator(config) + validator.country_code = "GB" bt.logging.off() mock = AsyncMock(aioredis.Redis) diff --git a/tests/unit_tests/subnet/validator/test_latency_score.py b/tests/unit_tests/subnet/validator/test_latency_score.py index aee3032d..1dbe4ae6 100644 --- a/tests/unit_tests/subnet/validator/test_latency_score.py +++ b/tests/unit_tests/subnet/validator/test_latency_score.py @@ -2,13 +2,17 @@ import tests.unit_tests.mocks.mock_miners as mocks +locations = { + "DE": {"country": "Germany", "latitude": 51.165691, "longitude": 10.451526}, +} + def test_a_not_verified_miner_should_return_a_score_of_zero(): # Arrange miner = mocks.miner_not_verified_1 # Act - result = compute_latency_score(miner.country, miner, [miner]) + result = compute_latency_score(miner.country, miner, [miner], locations) # Assert assert 0.0 == result @@ -19,7 +23,7 @@ def test_an_ip_conflicts_miner_should_return_a_score_of_zero(): miner = mocks.miner_with_ip_conflicts_1 # Act - result = compute_latency_score(miner.country, miner, [miner]) + result = compute_latency_score(miner.country, miner, [miner], locations) # Assert assert 0.0 == result @@ -30,7 +34,7 @@ def test_a_not_verified_and_ip_conflicts_miner_should_return_a_score_of_zero(): miner = mocks.miner_not_verified_and_ip_conflicts_1 # Act - result = compute_latency_score(miner.country, miner, [miner]) + result = compute_latency_score(miner.country, miner, [miner], locations) # Assert assert 0.0 == result @@ -43,7 +47,7 @@ def test_a_verified_miner_when_alone_should_return_a_score_of_one(): miners = [miner] # Act - result = compute_latency_score(miner.country, miner, miners) + result = compute_latency_score(miner.country, miner, miners, locations) # Assert assert 1.0 == result @@ -60,7 +64,7 @@ def test_a_verified_miner_when_other_miners_are_not_verified_should_return_a_sco ] # Act - result = compute_latency_score(miner.country, miner, miners) + result = compute_latency_score(miner.country, miner, miners, locations) # Assert assert 1.0 == result @@ -73,7 +77,7 @@ def test_a_verified_miner_when_other_miners_have_ip_conflicts_should_return_a_sc miners = [miner, mocks.miner_with_ip_conflicts_1, mocks.miner_with_ip_conflicts_2] # Act - result = compute_latency_score(miner.country, miner, miners) + result = compute_latency_score(miner.country, miner, miners, locations) # Assert assert 1.0 == result @@ -86,7 +90,7 @@ def test_a_verified_miner_when_other_miners_are_not_verified_and_have_ip_conflic miners = [miner, mocks.miner_with_ip_conflicts_1, mocks.miner_with_ip_conflicts_2] # Act - result = compute_latency_score(miner.country, miner, miners) + result = compute_latency_score(miner.country, miner, miners, locations) # Assert assert 1.0 == result @@ -102,7 +106,7 @@ def test_a_verified_miner_when_evaluating_the_best_one_should_return_a_score_of_ ] # Act - result = compute_latency_score(miner.country, miner, miners) + result = compute_latency_score(miner.country, miner, miners, locations) # Assert assert 1.0 == result @@ -118,7 +122,7 @@ def test_a_verified_miner_when_evaluating_the_worst_one_should_return_a_score_of ] # Act - result = compute_latency_score(miner.country, miner, miners) + result = compute_latency_score(miner.country, miner, miners, locations) # Assert assert 0.0 == result @@ -134,7 +138,7 @@ def test_a_verified_miner_when_evaluating_a_middle_one_should_return_a_score_bet ] # Act - result = compute_latency_score(miner.country, miner, miners) + result = compute_latency_score(miner.country, miner, miners, locations) # Assert assert 0.5 == result @@ -150,7 +154,7 @@ def test_a_verified_miner_when_evaluating_a_30_percent_close_to_the_best_should_ ] # Act - result = compute_latency_score(miner.country, miner, miners) + result = compute_latency_score(miner.country, miner, miners, locations) # Assert assert 0.7 == result @@ -166,7 +170,7 @@ def test_a_verified_miner_when_evaluating_a_30_percent_close_to_the_worst_should ] # Act - result = compute_latency_score(miner.country, miner, miners) + result = compute_latency_score(miner.country, miner, miners, locations) # Assert assert abs(0.3 - result) < 0.000000000000001 diff --git a/tests/unit_tests/subnet/validator/test_resync_miners.py b/tests/unit_tests/subnet/validator/test_resync_miners.py index d84ffb8d..683a870f 100644 --- a/tests/unit_tests/subnet/validator/test_resync_miners.py +++ b/tests/unit_tests/subnet/validator/test_resync_miners.py @@ -1,7 +1,7 @@ import copy import pytest import unittest -from unittest.mock import patch +from unittest.mock import MagicMock, patch from tests.unit_tests.mocks import mock_redis, mock_country from tests.unit_tests.utils.metagraph import ( @@ -34,6 +34,15 @@ {"ip": "9.91.241.48", "country": "ES"}, ] +locations = { + "US": {"country": "United States", "latitude": 37.09024, "longitude": -95.712891}, + "SG": {"country": "Singapore", "latitude": 1.352083, "longitude": 103.819836}, + "BR": {"country": "Brazil", "latitude": -14.235004, "longitude": -51.92528}, + "ES": {"country": "Spain", "latitude": 40.463667, "longitude": -3.74922}, + "KR": {"country": "South Korea", "latitude": 35.907757, "longitude": 127.766922}, + "IR": {"country": "Iran", "latitude": 32.427908, "longitude": 53.688046}, +} + class TestResyncMiners(unittest.IsolatedAsyncioTestCase): @pytest.fixture(autouse=True) @@ -47,10 +56,8 @@ def tearDown(self): self.validator.subtensor.chain_state = self.chain_state sync_metagraph(self.validator, default_axons_details) - @patch("subnet.validator.miner.get_country") async def test_given_a_metagraph_when_no_change_should_return_the_same_list_of_miners( self, - mock_get_country, ): # Arrange axons_details = copy.deepcopy(default_axons_details) @@ -59,7 +66,11 @@ async def test_given_a_metagraph_when_no_change_should_return_the_same_list_of_m for idx, axon in enumerate(axons): axon.ip = axons_details[idx]["ip"] - mock_country.mock_get_country(mock_get_country, axons_details) + self.validator.country_service = MagicMock() + mock_country.mock_get_country( + self.validator.country_service.get_country, axons_details + ) + self.validator.database = mock_redis.mock_get_statistics( self.validator.metagraph.hotkeys ) @@ -74,17 +85,18 @@ async def test_given_a_metagraph_when_no_change_should_return_the_same_list_of_m # Assert assert miners == self.validator.miners - @patch("subnet.validator.miner.get_country") async def test_given_a_partially_full_metagraph_when_a_new_neuron_is_added_should_be_added_to_the_list( self, - mock_get_country, ): # Arrange axons_details = copy.deepcopy(default_axons_details) + [ {"ip": "19.91.241.48", "country": "US"} ] - mock_country.mock_get_country(mock_get_country, axons_details) + self.validator.country_service = MagicMock() + mock_country.mock_get_country( + self.validator.country_service.get_country, axons_details + ) self.validator.database = mock_redis.mock_get_statistics( self.validator.metagraph.hotkeys ) @@ -125,19 +137,22 @@ async def test_given_a_partially_full_metagraph_when_a_new_neuron_is_added_shoul assert False == miner.sync assert False == miner.verified - @patch("subnet.validator.miner.get_country") async def test_given_a_full_metagraph_when_a_uid_has_a_new_hotkey_with_same_ip_should_replace_the_old_miner_by_the_new_one_in_the_list( self, - mock_get_country, ): # Arrange axons_details = copy.deepcopy(default_axons_details) - mock_country.mock_get_country(mock_get_country, axons_details) + self.validator.country_service = MagicMock() + mock_country.mock_get_country( + self.validator.country_service.get_country, axons_details + ) self.validator.database = mock_redis.mock_get_statistics( self.validator.metagraph.hotkeys ) + self.validator.country_service.get_locations.return_value = locations + sync_metagraph(self.validator, axons_details) miners = await get_all_miners(self.validator) self.validator.miners = copy.deepcopy(miners) @@ -172,19 +187,22 @@ async def test_given_a_full_metagraph_when_a_uid_has_a_new_hotkey_with_same_ip_s assert False == miner.sync assert False == miner.verified - @patch("subnet.validator.miner.get_country") async def test_given_a_full_metagraph_when_a_uid_has_a_same_hotkey_with_different_ip_should_replace_the_old_miner_by_the_new_one_in_the_list( self, - mock_get_country, ): # Arrange axons_details = copy.deepcopy(default_axons_details) - mock_country.mock_get_country(mock_get_country, axons_details) + self.validator.country_service = MagicMock() + mock_country.mock_get_country( + self.validator.country_service.get_country, axons_details + ) self.validator.database = mock_redis.mock_get_statistics( self.validator.metagraph.hotkeys ) + self.validator.country_service.get_locations.return_value = locations + sync_metagraph(self.validator, axons_details) miners = await get_all_miners(self.validator) self.validator.miners = copy.deepcopy(miners) @@ -220,19 +238,22 @@ async def test_given_a_full_metagraph_when_a_uid_has_a_same_hotkey_with_differen assert False == miner.sync assert False == miner.verified - @patch("subnet.validator.miner.get_country") async def test_given_a_full_metagraph_when_a_uid_has_a_new_hotkey_with_different_ip_should_replace_the_old_miner_by_the_new_one_in_the_list( self, - mock_get_country, ): # Arrange axons_details = copy.deepcopy(default_axons_details) - mock_country.mock_get_country(mock_get_country, axons_details) + self.validator.country_service = MagicMock() + mock_country.mock_get_country( + self.validator.country_service.get_country, axons_details + ) self.validator.database = mock_redis.mock_get_statistics( self.validator.metagraph.hotkeys ) + self.validator.country_service.get_locations.return_value = locations + sync_metagraph(self.validator, axons_details) miners = await get_all_miners(self.validator) self.validator.miners = copy.deepcopy(miners) @@ -267,19 +288,22 @@ async def test_given_a_full_metagraph_when_a_uid_has_a_new_hotkey_with_different assert False == miner.sync assert False == miner.verified - @patch("subnet.validator.miner.get_country") async def test_given_a_metagraph_when_a_uid_is_not_running_should_be_removed_from_the_list( self, - mock_get_country, ): # Arrange axons_details = copy.deepcopy(default_axons_details) - mock_country.mock_get_country(mock_get_country, axons_details) + self.validator.country_service = MagicMock() + mock_country.mock_get_country( + self.validator.country_service.get_country, axons_details + ) self.validator.database = mock_redis.mock_get_statistics( self.validator.metagraph.hotkeys ) + self.validator.country_service.get_locations.return_value = locations + sync_metagraph(self.validator, axons_details) miners = await get_all_miners(self.validator) self.validator.miners = copy.deepcopy(miners) From 38f7cabce2d1f9bf1c5f99fb2495ba0829cbacf1 Mon Sep 17 00:00:00 2001 From: eclipsevortex Date: Thu, 30 May 2024 22:26:06 -0400 Subject: [PATCH 3/6] fix subtensor --- scripts/subtensor/scripts/subtensor_start.sh | 139 +++++++++---------- 1 file changed, 68 insertions(+), 71 deletions(-) diff --git a/scripts/subtensor/scripts/subtensor_start.sh b/scripts/subtensor/scripts/subtensor_start.sh index 09517531..9384e721 100755 --- a/scripts/subtensor/scripts/subtensor_start.sh +++ b/scripts/subtensor/scripts/subtensor_start.sh @@ -9,7 +9,7 @@ function run_command() F_NETWORK=$1 F_NODE_TYPE=$2 F_BIN_PATH=$3 - + # Different command options by network and node type MAINNET_CHAIN='--chain ./raw_spec.json' TESTNET_CHAIN='--chain ./raw_testspec.json' @@ -18,50 +18,47 @@ function run_command() TESTNET_BOOTNODE='--bootnodes /dns/bootnode.test.finney.opentensor.ai/tcp/30333/p2p/12D3KooWPM4mLcKJGtyVtkggqdG84zWrd7Rij6PGQDoijh1X86Vr' NODE_TYPE_ARCHIVE='--pruning=archive' NODE_TYPE_LITE='--sync warp' - - + + # Options by the type of node we offer MAINNET_ARCHIVE_OPTIONS="$MAINNET_CHAIN $MAINNET_BOOTNODE $NODE_TYPE_ARCHIVE" MAINNET_LITE_OPTIONS="$MAINNET_CHAIN $MAINNET_BOOTNODE $NODE_TYPE_LITE" TESTNET_ARCHIVE_OPTIONS="$TESTNET_CHAIN $TESTNET_BOOTNODE $NODE_TYPE_ARCHIVE" TESTNET_LITE_OPTIONS="$TESTNET_CHAIN $TESTNET_BOOTNODE $NODE_TYPE_LITE" - + # Checking options to use if [[ "$F_NETWORK" == "mainnet" ]] && [[ "$F_NODE_TYPE" == "archive" ]]; then SPECIFIC_OPTIONS=$MAINNET_ARCHIVE_OPTIONS - elif [[ "$F_NETWORK" == "mainnet" ]] && [[ "$F_NODE_TYPE" == "lite" ]]; then - SPECIFIC_OPTIONS=$MAINNET_LITE_OPTIONS - elif [[ "$F_NETWORK" == "testnet" ]] && [[ "$F_NODE_TYPE" == "archive" ]]; then - SPECIFIC_OPTIONS=$TESTNET_ARCHIVE_OPTIONS - elif [[ "$F_NETWORK" == "testnet" ]] && [[ "$F_NODE_TYPE" == "lite" ]]; then - SPECIFIC_OPTIONS=$TESTNET_LITE_OPTIONS + elif [[ "$F_NETWORK" == "mainnet" ]] && [[ "$F_NODE_TYPE" == "lite" ]]; then + SPECIFIC_OPTIONS=$MAINNET_LITE_OPTIONS + elif [[ "$F_NETWORK" == "testnet" ]] && [[ "$F_NODE_TYPE" == "archive" ]]; then + SPECIFIC_OPTIONS=$TESTNET_ARCHIVE_OPTIONS + elif [[ "$F_NETWORK" == "testnet" ]] && [[ "$F_NODE_TYPE" == "lite" ]]; then + SPECIFIC_OPTIONS=$TESTNET_LITE_OPTIONS fi - + if [ ! -f $F_BIN_PATH ]; then echo "Binary '$F_BIN_PATH' does not exist. You can use -p or --bin-path to specify a different location." echo "Please ensure you have compiled the binary first." exit 1 fi - + # Command to run subtensor $F_BIN_PATH \ - --base-path /tmp/blockchain \ - --execution wasm \ - --wasm-execution compiled \ - --max-runtime-instances 32 \ - --rpc-max-response-size 2048 \ - --rpc-external \ - --rpc-cors all \ - --no-mdns \ - --ws-max-connections 16000 \ - --in-peers 8000 \ - --out-peers 8000 \ - --prometheus-external \ - --rpc-port 9933 \ - --port 30333 \ - --ws-port 9944 \ - --ws-external \ - $SPECIFIC_OPTIONS + --base-path /tmp/blockchain \ + --execution wasm \ + --wasm-execution compiled \ + --port 30333 \ + --max-runtime-instances 64 \ + --rpc-max-response-size 2048 \ + --rpc-cors all \ + --rpc-port 9944 \ + --no-mdns \ + --in-peers 8000 \ + --out-peers 8000 \ + --prometheus-external \ + --rpc-external \ + $SPECIFIC_OPTIONS } @@ -74,44 +71,44 @@ BIN_PATH="./target/release/node-subtensor" # Getting arguments from user while [[ $# -gt 0 ]]; do - case $1 in - -h|--help) - help - exit 0 - ;; - -e|--execution) - EXEC_TYPE="$2" - shift # past argument - shift # past value - ;; - -b|--build) - BUILD="--build" - shift # past argument - ;; - -n|--network) - NETWORK="$2" - shift - shift - ;; - -t|--node-type) - NODE_TYPE="$2" - shift - shift - ;; - -p|--bin-path) - BIN_PATH="$2" - shift - shift - ;; - -*|--*) - echo "Unknown option $1" - exit 1 - ;; - *) - POSITIONAL_ARGS+=("$1") - shift - ;; - esac + case $1 in + -h|--help) + help + exit 0 + ;; + -e|--execution) + EXEC_TYPE="$2" + shift # past argument + shift # past value + ;; + -b|--build) + BUILD="--build" + shift # past argument + ;; + -n|--network) + NETWORK="$2" + shift + shift + ;; + -t|--node-type) + NODE_TYPE="$2" + shift + shift + ;; + -p|--bin-path) + BIN_PATH="$2" + shift + shift + ;; + -*|--*) + echo "Unknown option $1" + exit 1 + ;; + *) + POSITIONAL_ARGS+=("$1") + shift + ;; + esac done # Verifying arguments values @@ -133,10 +130,10 @@ fi # Running subtensor case $EXEC_TYPE in docker) - cd $HOME/subtensor - docker compose down --remove-orphans - echo "Running docker compose up $BUILD --detach $NETWORK-$NODE_TYPE" - docker compose up $BUILD --detach $NETWORK-$NODE_TYPE + cd $HOME/subtensor + docker compose down --remove-orphans + echo "Running docker compose up $BUILD --detach $NETWORK-$NODE_TYPE" + docker compose up $BUILD --detach $NETWORK-$NODE_TYPE ;; binary) run_command $NETWORK $NODE_TYPE $BIN_PATH From 4e20fb8e0ebc9f9b1d9c4871d721ede682c52850 Mon Sep 17 00:00:00 2001 From: eclipsevortex Date: Thu, 30 May 2024 23:31:05 -0400 Subject: [PATCH 4/6] adjust weight for the scores --- subnet/constants.py | 8 ++++---- subnet/validator/score.py | 2 +- tests/unit_tests/subnet/validator/test_final_score.py | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/subnet/constants.py b/subnet/constants.py index 4d761391..9aa069cd 100644 --- a/subnet/constants.py +++ b/subnet/constants.py @@ -5,10 +5,10 @@ RELIABILLITY_FAILURE_REWARD = 0.0 # Score weights -AVAILABILITY_WEIGHT = 3.0 -LATENCY_WEIGHT = 1.0 -RELIABILLITY_WEIGHT = 1.0 -DISTRIBUTION_WEIGHT = 1.0 +AVAILABILITY_WEIGHT = 8 +LATENCY_WEIGHT = 8 +RELIABILLITY_WEIGHT = 3 +DISTRIBUTION_WEIGHT = 1 # Reset config RELIABILLITY_RESET = 1080 # Reset challenge statistics every 3 epochs diff --git a/subnet/validator/score.py b/subnet/validator/score.py index de7a33dc..68108500 100644 --- a/subnet/validator/score.py +++ b/subnet/validator/score.py @@ -215,7 +215,7 @@ def compute_final_score(miner: Miner): """ # Use a smaller weight if the subtensor is available but desync (miner block < validator block - 1) availability_weight = ( - 1 if miner.verified and not miner.sync else AVAILABILITY_WEIGHT + 3 if miner.verified and not miner.sync else AVAILABILITY_WEIGHT ) numerator = ( diff --git a/tests/unit_tests/subnet/validator/test_final_score.py b/tests/unit_tests/subnet/validator/test_final_score.py index 2719e4b8..e1525baa 100644 --- a/tests/unit_tests/subnet/validator/test_final_score.py +++ b/tests/unit_tests/subnet/validator/test_final_score.py @@ -30,7 +30,7 @@ def test_a_not_sync_miner_should_return_a_score_different_of_zero(): miner.reliability_score = 0.30 miner.distribution_score = 0.40 - expected_score = (0.10 + 0.20 + 0.30 + 0.40) / 4 + expected_score = (0.10 * 3 + 0.20 * 8 + 0.30 * 3 + 0.40) / 15 # Act result = compute_final_score(miner) @@ -67,7 +67,7 @@ def test_a_suspicious_miner_with_penalty_factor_should_return_a_score_different_ miner.distribution_score = 0.40 miner.penalty_factor = 0.4 - expected_score = ((0.10 + 0.20 + 0.30 + 0.40) / 4) * 0.4 + expected_score = ((0.10 * 3 + 0.20 * 8 + 0.30 * 3 + 0.40) / 15) * 0.4 # Act result = compute_final_score(miner) @@ -86,7 +86,7 @@ def test_a_verified_and_sync_miner_should_return_a_score_different_of_zero(): miner.reliability_score = 0.30 miner.distribution_score = 0.40 - expected_score = (3 * 0.10 + 0.20 + 0.30 + 0.40) / 6 + expected_score = (0.10 * 8 + 0.20 * 8 + 0.30 * 3 + 0.40) / 20 # Act result = compute_final_score(miner) From 19df8c99f164807bf2a9bc5a8e9cc2bf0f3075cb Mon Sep 17 00:00:00 2001 From: eclipsevortex Date: Fri, 31 May 2024 07:44:59 -0400 Subject: [PATCH 5/6] prepare new release --- CHANGELOG.md | 11 ++ VERSION | 2 +- .../release/release-2.2.5/RELEASE-2.2.5.md | 152 ++++++++++++++++++ subnet/__init__.py | 2 +- 4 files changed, 165 insertions(+), 2 deletions(-) create mode 100644 scripts/release/release-2.2.5/RELEASE-2.2.5.md diff --git a/CHANGELOG.md b/CHANGELOG.md index a14773ff..1120c551 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## 2.2.5 / 2024-05-31 + +## What's Changed +* resyn metagraph for miner by @eclipsevortex in https://github.com/eclipsevortex/SubVortex/pull/62 +* fix subtensor by @eclipsevortex in https://github.com/eclipsevortex/SubVortex/pull/65 +* fix country api by @eclipsevortex in https://github.com/eclipsevortex/SubVortex/pull/64 +* adjust weight for the scores by @eclipsevortex in https://github.com/eclipsevortex/SubVortex/pull/66 + + +**Full Changelog**: https://github.com/eclipsevortex/SubVortex/compare/v2.2.4...v2.2.5 + ## 2.2.4 / 2024-05-20 ## What's Changed diff --git a/VERSION b/VERSION index 04761555..ecf00d90 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.2.4 \ No newline at end of file +2.2.5 \ No newline at end of file diff --git a/scripts/release/release-2.2.5/RELEASE-2.2.5.md b/scripts/release/release-2.2.5/RELEASE-2.2.5.md new file mode 100644 index 00000000..f95f582f --- /dev/null +++ b/scripts/release/release-2.2.5/RELEASE-2.2.5.md @@ -0,0 +1,152 @@ +This guide provides step-by-step instructions for the release 2.2.5. + +Previous Release: 2.2.4 + +
+ +--- + +- [Validator](#validators) + - [Rollout Process](#validator-rollout-process) + - [Rollback Process](#validator-rollback-process) +- [Miner](#miner) + - [Rollout Process](#miner-rollout-process) + - [Rollback Process](#miner-rollback-process) +- [Additional Resources](#additional-resources) +- [Troubleshooting](#troubleshooting) + +--- + +
+ +# Validator + +## Rollout Process + +1. **Upgrade Subnet**: Fetch the remote tags + + ```bash + git fetch --tags --force + ``` + + Then, checkout the new release tag + + ```bash + git checkout tags/v2.2.5 + ``` + + Finally, install the dependencies + + ```bash + pip install --upgrade SubVortex + pip install -e . + ``` + +2. **Restart validator**: Restart your validator to take the new version + + ```bash + pm2 restart validator-7 + ``` + +3. **Check logs**: Check the validator logs to see if you see some `New Block` + ```bash + pm2 logs validator-7 + ``` + +
+ +## Rollback Process + +If any issues arise during or after the rollout, follow these steps to perform a rollback: + +1. **Downgrade Subnet**: Checkout the previous release tag + + ```bash + git checkout tags/v2.2.4 + ``` + + Then, install the dependencies + + ```bash + pip install --upgrade SubVortex + pip install -e . + ``` + +2. **Restart validator**: Restart your validator to take the old version + + ```bash + pm2 restart validator-7 + ``` + +3. **Check logs**: Check the validator logs to see if you see some `New Block` + ```bash + pm2 logs validator-7 + ``` + +
+ +# Miner + +## Rollout Process + +1. **Upgrade Subnet**: Fetch the remote tags + + ```bash + git fetch --tags --force + ``` + + Then, checkout the new release tag + + ```bash + git checkout tags/v2.2.5 + ``` + + Finally, install the dependencies + + ```bash + pip install --upgrade SubVortex + pip install -e . + ``` + +2. **Restart miner**: Restart your miner to take the new version + + ```bash + pm2 restart miner-7 + ``` + +3. **Check logs**: Check the miner logs to see if you see some `New Block` + ```bash + pm2 logs miner-7 + ``` + +## Rollback Process + +1. **Downgrade Subnet**: Checkout the previous release tag + + ```bash + git checkout tags/v2.2.4 + ``` + + Then, install the dependencies + + ```bash + pip install --upgrade SubVortex + pip install -e . + ``` + +2. **Restart miner**: Restart your miner to take the old version + + ```bash + pm2 restart miner-7 + ``` + +3. **Check logs**: Check the miner logs to see if you see some `New Block` + ```bash + pm2 logs miner-7 + ``` + +
+ +# Additional Resources + +For any further assistance or inquiries, please contact [**SubVortex Team**](https://discord.com/channels/799672011265015819/1215311984799653918) \ No newline at end of file diff --git a/subnet/__init__.py b/subnet/__init__.py index 3c5e85ac..9e9b7903 100644 --- a/subnet/__init__.py +++ b/subnet/__init__.py @@ -50,7 +50,7 @@ def __lt__(self, other): ) -__version__ = "2.2.4" +__version__ = "2.2.5" version = SubnetVersion.from_string(__version__) __spec_version__ = version.to_spec_version() From faa3f147a6ea7d418c8fd41a17b509b0426aad67 Mon Sep 17 00:00:00 2001 From: eclipsevortex Date: Fri, 31 May 2024 18:14:17 -0400 Subject: [PATCH 6/6] fix last details --- README.md | 3 +- docs/features/incentive_mechanism.md | 8 ++--- subnet/constants.py | 4 +-- subnet/country/country.py | 35 +++++++++++++++---- .../subnet/validator/test_final_score.py | 6 ++-- 5 files changed, 40 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index e668cc08..4e4f0800 100644 --- a/README.md +++ b/README.md @@ -141,7 +141,7 @@ Team responsabilities - **EclipseVortex** - Development and technology - **Ch3RNØbØG** - Operations and business development -- **tww9** - Strategy and public relations +- **CryptoMinedMind** - Strategy and public relations - **HcL-CO** - QA Lead and support Team timezone @@ -182,6 +182,7 @@ Bittensor technology is still new and promising, and participants are eager to s - **Bittensor**: for providing a subnet template that enabled us to quickly set up our subnet. - **Subtensor**: for their local subtensor, scripts, and invaluable assistance. +- **andrewoflaherty**: for implementing the country api using MaxMind and IPInfo ([github](https://github.com/OFlahertyAndrew)) - **Subnet Storage (SN21)**: for their excellent subnet design, which adheres to best practices and simplifies the lives of developers. - **Users**: YES!!! Without you, we are nothing, and our vision to advance this new technology would never have materialized. - **Others**: undoubtedly, there are many other contributors deserving of recognition, and we look forward to acknowledging them in the future. diff --git a/docs/features/incentive_mechanism.md b/docs/features/incentive_mechanism.md index 6b5e377b..adb39911 100644 --- a/docs/features/incentive_mechanism.md +++ b/docs/features/incentive_mechanism.md @@ -74,10 +74,10 @@ The final score is the score for the current challenge. It will be computed as t The weight for each score will be as follows: -- Availability: Weight will be **3** if the miner's subtensor is synchronized, and **1** otherwise. -- Latency: Weight will be **1**. -- Reliability: Weight will be **1**. -- Distribution: Weight will be **1**. +- Availability: Weight will be **8** if the miner's subtensor is synchronized, and **3** otherwise. +- Latency: Weight will be **7**. +- Reliability: Weight will be **3**. +- Distribution: Weight will be **2**. So, the final formula will be calculated using the following expression diff --git a/subnet/constants.py b/subnet/constants.py index 9aa069cd..9d0116f0 100644 --- a/subnet/constants.py +++ b/subnet/constants.py @@ -6,9 +6,9 @@ # Score weights AVAILABILITY_WEIGHT = 8 -LATENCY_WEIGHT = 8 +LATENCY_WEIGHT = 7 RELIABILLITY_WEIGHT = 3 -DISTRIBUTION_WEIGHT = 1 +DISTRIBUTION_WEIGHT = 2 # Reset config RELIABILLITY_RESET = 1080 # Reset challenge statistics every 3 epochs diff --git a/subnet/country/country.py b/subnet/country/country.py index 5bb4b056..431f2706 100644 --- a/subnet/country/country.py +++ b/subnet/country/country.py @@ -2,6 +2,7 @@ import copy import requests import threading +import ipaddress import bittensor as bt from datetime import datetime from typing import List @@ -46,38 +47,60 @@ def get_locations(self) -> List[str]: localisations = self._data.get("localisations") or {} return copy.deepcopy(localisations) + def get_ipv4(self, ip): + try: + # First, try to interpret the input as an IPv4 address + ipv4 = ipaddress.IPv4Address(ip) + return str(ipv4) + except ipaddress.AddressValueError: + pass + + try: + # Next, try to interpret the input as an IPv6 address + ipv6 = ipaddress.IPv6Address(ip) + if ipv6.ipv4_mapped: + return str(ipv6.ipv4_mapped) + except ipaddress.AddressValueError: + pass + + return ip + def get_country(self, ip: str): """ Get the country code of the ip """ + ip_ipv4 = self.get_ipv4(ip) + country = None with self._lock: overrides = self._data.get("overrides") or {} - country = overrides.get(ip) + country = overrides.get(ip_ipv4) if country: return country country, reason1 = ( - get_country_by_my_api(ip) if self._is_custom_api_enabled() else (None, None) + get_country_by_my_api(ip_ipv4) + if self._is_custom_api_enabled() + else (None, None) ) if country: return country - country, reason2 = get_country_by_country_is(ip) + country, reason2 = get_country_by_country_is(ip_ipv4) if country: return country - country, reason3 = get_country_by_ip_api(ip) + country, reason3 = get_country_by_ip_api(ip_ipv4) if country: return country - country, reason4 = get_country_by_ipinfo_io(ip) + country, reason4 = get_country_by_ipinfo_io(ip_ipv4) if country: return country bt.logging.warning( - f"Could not get the country of the ip {ip}: Api 1: {reason1} / Api 2: {reason2} / Api 3: {reason3} / Api 4: {reason4}" + f"Could not get the country of the ip {ip_ipv4}: Api 1: {reason1} / Api 2: {reason2} / Api 3: {reason3} / Api 4: {reason4}" ) return None diff --git a/tests/unit_tests/subnet/validator/test_final_score.py b/tests/unit_tests/subnet/validator/test_final_score.py index e1525baa..d4fe1e26 100644 --- a/tests/unit_tests/subnet/validator/test_final_score.py +++ b/tests/unit_tests/subnet/validator/test_final_score.py @@ -30,7 +30,7 @@ def test_a_not_sync_miner_should_return_a_score_different_of_zero(): miner.reliability_score = 0.30 miner.distribution_score = 0.40 - expected_score = (0.10 * 3 + 0.20 * 8 + 0.30 * 3 + 0.40) / 15 + expected_score = (0.10 * 3 + 0.20 * 7 + 0.30 * 3 + 0.40 * 2) / 15 # Act result = compute_final_score(miner) @@ -67,7 +67,7 @@ def test_a_suspicious_miner_with_penalty_factor_should_return_a_score_different_ miner.distribution_score = 0.40 miner.penalty_factor = 0.4 - expected_score = ((0.10 * 3 + 0.20 * 8 + 0.30 * 3 + 0.40) / 15) * 0.4 + expected_score = ((0.10 * 3 + 0.20 * 7 + 0.30 * 3 + 0.40 * 2) / 15) * 0.4 # Act result = compute_final_score(miner) @@ -86,7 +86,7 @@ def test_a_verified_and_sync_miner_should_return_a_score_different_of_zero(): miner.reliability_score = 0.30 miner.distribution_score = 0.40 - expected_score = (0.10 * 8 + 0.20 * 8 + 0.30 * 3 + 0.40) / 20 + expected_score = (0.10 * 8 + 0.20 * 7 + 0.30 * 3 + 0.40 * 2) / 20 # Act result = compute_final_score(miner)