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
35 changes: 18 additions & 17 deletions neurons/base/validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -288,23 +288,23 @@ def resync_metagraph(self):
return

bt.logging.info('Metagraph updated, re-syncing hotkeys, dendrite pool and moving averages')
# Zero out all hotkeys that have been replaced.
for uid, hotkey in enumerate(self.hotkeys):
if hotkey != self.metagraph.hotkeys[uid]:
self.scores[uid] = 0 # hotkey has been replaced

# Check to see if the metagraph has changed size.
# If so, we need to add new hotkeys and moving averages.
if len(self.hotkeys) < len(self.metagraph.hotkeys):
# Update the size of the moving average scores.
new_moving_average = np.zeros((self.metagraph.n))
min_len = min(len(self.hotkeys), len(self.scores))
new_moving_average[:min_len] = self.scores[:min_len]
self.scores = new_moving_average

# Update the hotkeys.
self.scores = self._align_scores_to_metagraph(self.scores, self.hotkeys)
self.hotkeys = copy.deepcopy(self.metagraph.hotkeys)

def _align_scores_to_metagraph(self, scores: np.ndarray, hotkeys: list) -> np.ndarray:
"""Resize scores to the live metagraph and zero slots whose hotkey was replaced."""
n = int(self.metagraph.n)
scores_arr = np.asarray(scores, dtype=np.float32)
aligned = np.zeros(n, dtype=np.float32)
min_len = min(len(scores_arr), n)
aligned[:min_len] = scores_arr[:min_len]

for uid in range(min(len(hotkeys), n)):
if hotkeys[uid] != self.metagraph.hotkeys[uid]:
aligned[uid] = 0.0

return aligned

def update_scores(self, rewards: np.ndarray, uids: set[int], blacklisted_uids: List[int] = None):
"""Performs exponential moving average on the scores based on the rewards received from the miners."""

Expand Down Expand Up @@ -380,9 +380,10 @@ def load_state(self):
state_path = self.config.neuron.full_path + '/state.npz'
try:
state = np.load(state_path)
loaded_hotkeys = list(state['hotkeys'])
self.step = int(state['step'])
self.scores = state['scores']
self.hotkeys = list(state['hotkeys'])
self.scores = self._align_scores_to_metagraph(state['scores'], loaded_hotkeys)
self.hotkeys = copy.deepcopy(self.metagraph.hotkeys)
bt.logging.success(f'Successfully loaded validator state from {state_path}')
except FileNotFoundError:
bt.logging.warning(f'No state file found at {state_path}, starting with fresh state')
Expand Down
77 changes: 77 additions & 0 deletions tests/validator/test_validator_state_alignment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# The MIT License (MIT)
# Copyright © 2025 Entrius

"""Regression tests for validator state alignment after metagraph size changes."""

from unittest.mock import MagicMock

import numpy as np

from neurons.base.validator import BaseValidatorNeuron


class _ValidatorStub:
_align_scores_to_metagraph = BaseValidatorNeuron._align_scores_to_metagraph
load_state = BaseValidatorNeuron.load_state

metagraph: MagicMock
config: MagicMock
step: int
scores: np.ndarray
hotkeys: list[str]

def __init__(self, n: int, hotkeys: list[str]):
self.metagraph = MagicMock()
self.metagraph.n = n
self.metagraph.hotkeys = hotkeys


def test_align_scores_grows_with_metagraph():
validator = _ValidatorStub(n=4, hotkeys=['h0', 'h1', 'h2', 'h3'])
scores = np.array([0.1, 0.2, 0.3], dtype=np.float32)

aligned = validator._align_scores_to_metagraph(scores, ['h0', 'h1', 'h2'])

assert aligned.shape == (4,)
assert np.allclose(aligned[:3], [0.1, 0.2, 0.3])
assert aligned[3] == 0.0


def test_align_scores_shrinks_with_metagraph():
validator = _ValidatorStub(n=2, hotkeys=['h0', 'h1'])
scores = np.array([0.1, 0.2, 0.3, 0.4], dtype=np.float32)

aligned = validator._align_scores_to_metagraph(scores, ['h0', 'h1', 'h2', 'h3'])

assert aligned.shape == (2,)
assert np.allclose(aligned, [0.1, 0.2])


def test_align_scores_zeros_replaced_hotkey():
validator = _ValidatorStub(n=3, hotkeys=['h0', 'new_h1', 'h2'])
scores = np.array([0.1, 0.5, 0.4], dtype=np.float32)

aligned = validator._align_scores_to_metagraph(scores, ['h0', 'old_h1', 'h2'])

assert np.allclose(aligned, [0.1, 0.0, 0.4])


def test_load_state_realigns_persisted_scores_to_current_metagraph(tmp_path, monkeypatch):
validator = _ValidatorStub(n=4, hotkeys=['h0', 'h1', 'h2', 'h3'])
validator.config = MagicMock()
validator.config.neuron.full_path = str(tmp_path)
state_path = tmp_path / 'state.npz'
np.savez(
state_path,
step=42,
scores=np.array([0.1, 0.2, 0.3], dtype=np.float32),
hotkeys=['h0', 'h1', 'h2'],
)

validator.load_state()

assert validator.step == 42
assert validator.scores.shape == (4,)
assert np.allclose(validator.scores[:3], [0.1, 0.2, 0.3])
assert validator.scores[3] == 0.0
assert validator.hotkeys == ['h0', 'h1', 'h2', 'h3']
Loading