From e0620cf186ecc91ab4e17c7e8fb1ae0e25ab82d0 Mon Sep 17 00:00:00 2001 From: RealDiligent Date: Thu, 9 Jul 2026 06:57:37 +0800 Subject: [PATCH 1/4] fix: resolve validator state desync after metagraph resize on restart load_state() restored scores/hotkeys from state.npz without resizing to the current metagraph.n, causing IndexError or misaligned on-chain weights after subnet growth or shrink. Realign persisted scores on load and consolidate metagraph resize handling in resync_metagraph(). Co-authored-by: Cursor --- neurons/base/validator.py | 35 +++++----- .../test_validator_state_alignment.py | 68 +++++++++++++++++++ 2 files changed, 86 insertions(+), 17 deletions(-) create mode 100644 tests/validator/test_validator_state_alignment.py diff --git a/neurons/base/validator.py b/neurons/base/validator.py index 32fea9d5..57663b8b 100644 --- a/neurons/base/validator.py +++ b/neurons/base/validator.py @@ -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.""" @@ -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') diff --git a/tests/validator/test_validator_state_alignment.py b/tests/validator/test_validator_state_alignment.py new file mode 100644 index 00000000..d37c1f65 --- /dev/null +++ b/tests/validator/test_validator_state_alignment.py @@ -0,0 +1,68 @@ +# 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(BaseValidatorNeuron): + 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'] From 192f5a07cc85a44a4d1a269ebe726994075a01bf Mon Sep 17 00:00:00 2001 From: RealDiligent Date: Thu, 9 Jul 2026 07:00:31 +0800 Subject: [PATCH 2/4] ci: retrigger workflow Co-authored-by: Cursor From e1adb03033e1db4cc2b5161e1ee6961910484156 Mon Sep 17 00:00:00 2001 From: RealDiligent Date: Thu, 9 Jul 2026 07:10:18 +0800 Subject: [PATCH 3/4] test: avoid abstract BaseValidatorNeuron stub in state alignment tests Co-authored-by: Cursor --- tests/validator/test_validator_state_alignment.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/validator/test_validator_state_alignment.py b/tests/validator/test_validator_state_alignment.py index d37c1f65..cb2b4f11 100644 --- a/tests/validator/test_validator_state_alignment.py +++ b/tests/validator/test_validator_state_alignment.py @@ -10,7 +10,10 @@ from neurons.base.validator import BaseValidatorNeuron -class _ValidatorStub(BaseValidatorNeuron): +class _ValidatorStub: + _align_scores_to_metagraph = BaseValidatorNeuron._align_scores_to_metagraph + load_state = BaseValidatorNeuron.load_state + def __init__(self, n: int, hotkeys: list[str]): self.metagraph = MagicMock() self.metagraph.n = n From d28d21f821b23e53e756620f953775c54d222c5b Mon Sep 17 00:00:00 2001 From: RealDiligent Date: Thu, 9 Jul 2026 07:13:47 +0800 Subject: [PATCH 4/4] test: add typed attrs to validator state alignment stub Co-authored-by: Cursor --- tests/validator/test_validator_state_alignment.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/validator/test_validator_state_alignment.py b/tests/validator/test_validator_state_alignment.py index cb2b4f11..5f404a26 100644 --- a/tests/validator/test_validator_state_alignment.py +++ b/tests/validator/test_validator_state_alignment.py @@ -14,6 +14,12 @@ 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