Skip to content

fix: clear Active/ValidatorTrust/ValidatorPermit on neuron replace#2609

Open
boskodev790 wants to merge 1 commit into
opentensor:mainfrom
boskodev790:fix/replace-neuron-clear-validator-permit-trust
Open

fix: clear Active/ValidatorTrust/ValidatorPermit on neuron replace#2609
boskodev790 wants to merge 1 commit into
opentensor:mainfrom
boskodev790:fix/replace-neuron-clear-validator-permit-trust

Conversation

@boskodev790
Copy link
Copy Markdown

@boskodev790 boskodev790 commented Apr 22, 2026

clear_neuron zeroed the per-UID slot of Emission, Consensus, Incentive, Dividends, StakeWeight, Bonds and Weights, but missed Active, ValidatorTrust, and ValidatorPermit. All three are Vec<_>-per-netuid storage (Active and ValidatorPermit are Vec via EmptyBoolVec; ValidatorTrust is Vec) that append_neuron initialises on a fresh UID, so when replace_neuron reuses a UID it currently left the old occupant's active=true flag, validator_permit=true flag, and validator_trust score in place until the next epoch recomputed the vectors.

Between replacement and the next epoch, RPC surfaces (delegate_info, show_subnet) and on-chain readers (trim_to_max_allowed_uids and the per-mechanism validator aggregation in mechanism.rs) observed stale validator/active state for the freshly registered neuron.

Same class of stale-inheritance bug PR #755 was introduced to fix; these three fields were missed there.

  • clear_neuron now also zeroes ValidatorTrust[uid] and resets ValidatorPermit[uid] = false, using the same set_element_at idiom already applied to the other vec-per-netuid fields two lines above.
  • Add test_replace_neuron_clears_validator_trust_and_permit mirroring the existing test_replace_neuron_resets_last_update style.

Description

Related Issue(s)

  • Closes #[issue number]

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update
  • Other (please describe):

Breaking Change

If this PR introduces a breaking change, please provide a detailed description of the impact and the migration path for existing applications.

Checklist

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have run ./scripts/fix_rust.sh to ensure my code is formatted and linted correctly
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • Any dependent changes have been merged and published in downstream modules

Screenshots (if applicable)

Please include any relevant screenshots or GIFs that demonstrate the changes made.

Additional Notes

Please provide any additional information or context that may be helpful for reviewers.

@boskodev790 boskodev790 reopened this May 8, 2026
@RUNECTZ33
Copy link
Copy Markdown

Fix looks correct — mirrors the existing set_element_at idiom for the other vec-per-netuid fields and the test follows the test_replace_neuron_resets_last_update pattern. Cross-ref to #755 in the PR body makes the bug class easy to verify.

One adjacent observation while reading through the per-netuid storage definitions:

Active::<T> is also a StorageMap<_, Identity, NetUid, Vec<bool>, ValueQuery, EmptyBoolVec<T>> — the same per-UID Vec<bool> shape as ValidatorPermit, populated during epoch. It looks like the same stale-inheritance class once a UID is reused (a replaced neuron would inherit the previous occupant's active=true flag until the next epoch's vector rebuild), but it isn't cleared by clear_neuron.

Is Active intentionally left to do_epoch's rebuild, or worth including in this PR (or a sibling)? LoadedEmission is the other per-netuid Vec that comes to mind but its shape is different and it's more recompute-driven.

clear_neuron zeroed the per-UID slot of Emission, Consensus, Incentive,
Dividends, StakeWeight, Bonds and Weights, but missed Active,
ValidatorTrust, and ValidatorPermit. All three are Vec<_>-per-netuid
storage (Active and ValidatorPermit are Vec<bool> via EmptyBoolVec;
ValidatorTrust is Vec<u16> via EmptyU16Vec) that append_neuron
initialises on a fresh UID, so when replace_neuron reuses a UID it
currently left the old occupant's active=true flag, validator_permit=true
flag, and validator_trust score in place until the next epoch
recomputed the vectors.

Between replacement and the next epoch, RPC surfaces (delegate_info,
show_subnet) and on-chain readers (trim_to_max_allowed_uids and the
per-mechanism validator aggregation in mechanism.rs) observed stale
validator/active state for the freshly registered neuron.

Same class of stale-inheritance bug PR opentensor#755 was introduced to fix;
these three fields were missed there.

- clear_neuron now also zeroes ValidatorTrust[uid] and resets
  ValidatorPermit[uid] = false and Active[uid] = false, using the same
  set_element_at idiom already applied to the other vec-per-netuid
  fields two lines above.
- Add test_replace_neuron_clears_validator_trust_and_permit and
  test_replace_neuron_clears_active mirroring the existing
  test_replace_neuron_resets_last_update style.

Co-authored-by: Cursor <cursoragent@cursor.com>
@boskodev790 boskodev790 force-pushed the fix/replace-neuron-clear-validator-permit-trust branch from 1938dc9 to e57f6f0 Compare May 11, 2026 19:32
@boskodev790 boskodev790 changed the title fix: clear ValidatorTrust/ValidatorPermit on neuron replace fix: clear Active/ValidatorTrust/ValidatorPermit on neuron replace May 11, 2026
@boskodev790
Copy link
Copy Markdown
Author

Thanks for the careful read, @RUNECTZ33 — the observation is right on the money.

Active::<T> is structurally identical to ValidatorPermit::<T> (both StorageMap<_, Identity, NetUid, Vec<bool>, ValueQuery, EmptyBoolVec<T>> in lib.rs) and was missed by the same omission in clear_neuron. A replaced neuron would inherit the previous occupant's active=true flag until the next epoch's vector rebuild, with the same downstream visibility (RPC surfaces, trim_to_max_allowed_uids, the per-mechanism aggregation in mechanism.rs).

I've folded the Active clear into this PR rather than spinning a sibling — it's the same fix class, the same line of code, and the same test pattern. Pushed as e57f6f0:

  • Active::<T>::mutate(netuid, |v| Self::set_element_at(v, neuron_index, false)); added to clear_neuron next to ValidatorPermit (kept the two Vec<bool> clears together)
  • test_replace_neuron_clears_active mirroring test_replace_neuron_clears_validator_trust_and_permit
  • PR title and body updated to reflect the expanded scope

On LoadedEmission: I agree it's the wrong shape for the set_element_at idiom — it's StorageMap<_, Identity, NetUid, Vec<(AccountId, u64, u64)>, OptionQuery>, indexed positionally by enumeration order rather than UID, and drain_emission fully replaces it pre-epoch rather than mutating it in place. Including it here would be a different fix shape (probably an OptionQuery clear or a filter on the AccountId), so I'd rather leave that to a separate PR if it turns out to be needed.

Let me know if the expanded scope still looks good.

— Bosko

@RUNECTZ33
Copy link
Copy Markdown

Expanded scope looks correct to me. Pulled e57f6f0 locally:

  • Active::<T>::mutate(netuid, |v| Self::set_element_at(v, neuron_index, false)); sits cleanly next to the ValidatorPermit clear — keeps the two Vec<bool> resets together, consistent with the surrounding set_element_at idiom.
  • test_replace_neuron_clears_active mirrors the trust+permit test exactly. Both setup (v.resize + v[idx] = true) and assertion (!Active::<Test>::get(netuid)[idx]) follow the established pattern. Good.

Agreed on the LoadedEmission call. Its (AccountId, u64, u64) tuple-positional layout means a UID-indexed set_element_at doesn't apply, and drain_emission rebuilding it pre-epoch reduces the stale-inheritance window. A separate PR (probably an OptionQuery::kill or AccountId filter) is the right shape if a real consumer turns out to read it cross-epoch.

LGTM on the expanded scope.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants