Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions data/default_departed_hotkeys.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion meta/meta.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"subnet_version": "7.0.1"
"subnet_version": "7.0.2"
}
15 changes: 15 additions & 0 deletions neurons/validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -952,6 +952,21 @@ def should_fail_early(self, synapse: template.protocol.SendSignal | template.pro
synapse.error_message = msg
return True

# don't process re-registered miners
if self.elimination_manager.is_hotkey_re_registered(synapse.dendrite.hotkey):
# Get deregistration timestamp and convert to human-readable date
departed_info = self.elimination_manager.departed_hotkeys.get(synapse.dendrite.hotkey, {})
detected_ms = departed_info.get("detected_ms", 0)
dereg_date = TimeUtil.millis_to_formatted_date_str(detected_ms) if detected_ms else "unknown"

msg = (f"This miner hotkey {synapse.dendrite.hotkey} was previously de-registered and is not allowed to re-register. "
f"De-registered on: {dereg_date} UTC. "
f"Re-registration is not permitted on this subnet.")
bt.logging.warning(msg)
synapse.successfully_processed = False
synapse.error_message = msg
return True

order_uuid = synapse.miner_order_uuid
tp = self.parse_trade_pair_from_signal(signal)
if order_uuid and self.uuid_tracker.exists(order_uuid):
Expand Down
6 changes: 4 additions & 2 deletions shared_objects/metagraph_updater.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from vali_objects.vali_config import ValiConfig, TradePair
from shared_objects.cache_controller import CacheController
from shared_objects.error_utils import ErrorUtils
from shared_objects.metagraph_utils import is_anomalous_hotkey_loss
from shared_objects.subtensor_lock import get_subtensor_lock
from time_util.time_util import TimeUtil

Expand Down Expand Up @@ -772,9 +773,10 @@ def update_metagraph(self):
if not lost_hotkeys and not gained_hotkeys:
bt.logging.info(f"metagraph hotkeys remain the same. n = {len(hotkeys_after)}")

percent_lost = 100 * len(lost_hotkeys) / len(hotkeys_before) if lost_hotkeys else 0
# Use shared anomaly detection logic
is_anomalous, percent_lost = is_anomalous_hotkey_loss(lost_hotkeys, len(hotkeys_before))
# failsafe condition to reject new metagraph
if len(lost_hotkeys) > 10 and percent_lost >= 25:
if is_anomalous:
error_msg = (f"Too many hotkeys lost in metagraph update: {len(lost_hotkeys)} hotkeys lost, "
f"{percent_lost:.2f}% of total hotkeys. Rejecting new metagraph. ALERT A TEAM MEMBER ASAP...")
bt.logging.error(error_msg)
Expand Down
61 changes: 61 additions & 0 deletions shared_objects/metagraph_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# developer: jbonilla
# Copyright © 2024 Taoshi Inc

"""
Shared utilities for metagraph analysis and anomaly detection.
"""

# Constants for anomaly detection
ANOMALY_DETECTION_MIN_LOST = 10 # Minimum number of lost hotkeys to trigger anomaly detection
ANOMALY_DETECTION_PERCENT_THRESHOLD = 25 # Percentage threshold for anomaly detection


def is_anomalous_hotkey_loss(lost_hotkeys: set, total_hotkeys_before: int) -> tuple[bool, float]:
"""
Detect anomalous drops in hotkey counts to avoid false positives from network issues.

This function identifies when too many hotkeys disappear at once, which likely indicates
a network connectivity issue rather than legitimate de-registrations. Both the absolute
count and percentage must exceed thresholds to trigger anomaly detection.

Args:
lost_hotkeys: Set of hotkeys that were lost in the metagraph update
total_hotkeys_before: Total number of hotkeys before the change

Returns:
tuple[bool, float]: (is_anomalous, percent_lost)
- is_anomalous: True if the change is anomalous (likely a network issue), False otherwise
- percent_lost: Percentage of hotkeys lost (0-100)

Examples:
>>> # Normal case: 5 hotkeys lost out of 100 (5%)
>>> is_anomalous_hotkey_loss({1, 2, 3, 4, 5}, 100)
(False, 5.0)

>>> # Anomalous case: 30 hotkeys lost out of 100 (30%)
>>> is_anomalous_hotkey_loss(set(range(30)), 100)
(True, 30.0)

>>> # Edge case: 15 hotkeys lost out of 40 (37.5% - high percentage but meets both thresholds)
>>> is_anomalous_hotkey_loss(set(range(15)), 40)
(True, 37.5)

>>> # Edge case: 11 hotkeys lost out of 100 (11% - above min count but below percent threshold)
>>> is_anomalous_hotkey_loss(set(range(11)), 100)
(False, 11.0)
"""
# Handle edge cases
if not lost_hotkeys or total_hotkeys_before == 0:
return False, 0.0

num_lost = len(lost_hotkeys)
percent_lost = 100.0 * num_lost / total_hotkeys_before

# Anomaly if we lost more than MIN_LOST hotkeys AND >= PERCENT_THRESHOLD of total
# Both conditions must be true to avoid false positives
is_anomalous = (
num_lost > ANOMALY_DETECTION_MIN_LOST and
percent_lost >= ANOMALY_DETECTION_PERCENT_THRESHOLD
)

return is_anomalous, percent_lost
24 changes: 24 additions & 0 deletions tests/vali_tests/mock_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,30 @@ def remove_hotkey(self, hotkey: str):
self.uid_to_hotkey = {i: hk for i, hk in enumerate(self.hotkeys)}
self.hotkey_to_uid = {hk: i for i, hk in enumerate(self.hotkeys)}

def add_hotkey(self, hotkey: str):
"""Add a hotkey to the metagraph (simulate re-registration)"""
if hotkey not in self.hotkeys:
self.hotkeys.append(hotkey)
self.n = len(self.hotkeys)

# Add to all lists with default values
new_uid = len(self.uids)
self.uids.append(new_uid)
self.stakes.append(100.0)
self.trust.append(1.0)
self.consensus.append(1.0)
self.incentive.append(1.0)
self.dividends.append(0.0)
self.active.append(1)
self.last_update.append(self.block)
self.validator_permit.append(True)
self.block_at_registration.append(self.block)

# Update mappings
self.uid_to_hotkey[new_uid] = hotkey
self.hotkey_to_uid[hotkey] = new_uid
self.uid_to_block[new_uid] = self.block


class EnhancedMockChallengePeriodManager(BaseMockChallengePeriodManager):
"""Enhanced mock challenge period manager with full bucket support"""
Expand Down
Loading
Loading