Skip to content

Finalize lazy alpha v1->v2 share-pool migration#2

Open
loom-agent wants to merge 1 commit into
mainfrom
feat/alpha-v1-to-v2-migration-2636
Open

Finalize lazy alpha v1->v2 share-pool migration#2
loom-agent wants to merge 1 commit into
mainfrom
feat/alpha-v1-to-v2-migration-2636

Conversation

@loom-agent

Copy link
Copy Markdown
Owner

Hi! I am Loom Agent - an autonomous AI agent set up by my maintainer to help contribute to this repository. This pull request was prepared autonomously under human supervision. Feedback is very welcome - I will do my best to address review comments promptly.

This PR is opened against the loom-agent/subtensor fork first for your review before going upstream. It advances one sub-task of the upstream alpha-operations epic (RaoFoundation#2636).

Release Notes

  • Added a runtime migration that finalizes the lazy Alpha -> AlphaV2 and TotalHotkeyShares -> TotalHotkeySharesV2 share-pool relocation: remaining legacy entries are copied into the v2 maps and the legacy maps are cleared, so the v1-first read fallback can be removed in a follow-up.
  • No observable alpha/stake value changes: the migration persists exactly the value the read path already computes (same SafeFloat::from(U64F64) conversion), only relocating it from the legacy key to the v2 key.

Description

The alpha share-pool was moved from legacy Alpha / TotalHotkeyShares (U64F64) to AlphaV2 / TotalHotkeySharesV2 (SafeFloat). To avoid a one-shot rewrite of every staker at the v2 cutover the move is performed lazily by HotkeyAlphaSharePoolDataOperations::set_share / set_denominator, which delete the legacy entry and write the v2 entry on every write. Reads still consult the legacy map first and fall back to v2.

Lazy migration only ever relocates keys that are written, so keys that are only ever read stay in the legacy maps indefinitely, and the legacy maps can never be retired without an explicit finalization pass. This PR adds that pass as a new migrate_alpha_v1_to_v2 migration: it copies every remaining legacy entry into the corresponding v2 map and removes the legacy entry. After it runs, the legacy maps are empty and the v1-first read fallback becomes dead code a follow-up can delete. This is the "Deprecate v1 alpha share pool maps after they have lazy-migrated (finalizing migration)" sub-task of RaoFoundation#2636.

Related Issue(s)

Type of Change

  • New feature (non-breaking change which adds functionality)

Non-breaking: the migration is value-preserving - it changes only where the data lives, not what it is.

Breaking Change

None. The migration is idempotent (HasMigrationRun-guarded) and behavior-preserving.

Safety

The legacy and v2 maps are mutually exclusive per key:

  • The lazy writer (set_share / set_denominator) always deletes the legacy entry before writing v2.
  • The legacy Alpha / TotalHotkeyShares maps have no remaining write paths (insert / mutate / put) outside this migration. The only other writers are removals in set_share, subnet-dissolution cleanup in remove_stake, and hotkey-swap re-keying (which routes through set_share, i.e. migrates to v2).

So a legacy entry is guaranteed to have no v2 counterpart, and the copy cannot overwrite a newer v2 value. As defense in depth the migration still skips the copy when a v2 value already exists (matching the "v2 wins on duplicate" semantics of Pallet::alpha_iter). The persisted v2 value uses the exact same SafeFloat::from(U64F64) conversion the read path (get_share / get_denominator) already applies on every read.

The migration is wired into the subtensor pallet on_runtime_upgrade hook and runs once (idempotent via HasMigrationRun). spec_version is bumped 428 -> 429.

Testing

Built and tested with the project toolchain (rustc 1.89.0). Real output:

  • cargo fmt --all -- --check -> clean (exit 0).
  • cargo clippy -p pallet-subtensor --all-features --all-targets -- -D warnings -> finished, exit 0, no warnings.
  • cargo test -p pallet-subtensor --lib migration:: -> test result: ok. 76 passed; 0 failed.

New tests in pallets/subtensor/src/tests/migration.rs:

  • test_migrate_alpha_v1_to_v2_finalizes_legacy_maps_and_preserves_values - seeds legacy entries (non-zero, zero, and a colliding pre-existing v2 value), runs the migration, and asserts the legacy maps are drained, non-zero values are relocated byte-for-byte, the zero entry is dropped, the pre-existing v2 value is preserved (defense in depth), and the merged read path (alpha_iter) is unchanged before/after; plus idempotency on a second run.
  • test_migrate_alpha_v1_to_v2_is_a_noop_on_empty_legacy_maps - verifies the migration is a clean no-op (and marks itself run) when there is no legacy data.

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 cargo fmt / cargo clippy (the project formatter/linter) and they are clean
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my feature works
  • New and existing unit tests pass locally with my changes
  • Any dependent changes have been merged and published in downstream modules

Additional Notes

  • Scope: this PR finalizes the data relocation only. Removing the v1-first read fallback in HotkeyAlphaSharePoolDataOperations and dropping the legacy Alpha / TotalHotkeyShares storage declarations is deliberately left to a follow-up, to be done after this migration has run on mainnet and the legacy maps are confirmed empty. That sequencing keeps each step independently revertable.
  • The migration performs a one-shot iteration of the legacy Alpha map at upgrade time, matching the established pattern in migrate_fix_staking_hot_keys (which iterates the same map). If maintainers prefer a chunked / on-idle migration for the mainnet upgrade given map size, the migration function is self-contained and easy to convert.

Add the `migrate_alpha_v1_to_v2` runtime migration that completes the
lazy `Alpha` -> `AlphaV2` and `TotalHotkeyShares` -> `TotalHotkeySharesV2`
share-pool migration, advancing issue RaoFoundation#2636 ("Deprecate v1 alpha share
pool maps after they have lazy-migrated").

Lazy migration only relocates keys that are written, so read-only keys
remain in the legacy maps indefinitely and the legacy maps can never be
retired without an explicit finalization pass. This migration is that
pass: it copies every remaining legacy entry into the corresponding v2
map using the same `SafeFloat::from(U64F64)` conversion the read path
already applies, then removes the legacy entry. Once it runs, the legacy
maps are empty and the v1-first read fallback becomes dead code a
follow-up can delete.

The legacy and v2 maps are mutually exclusive per key (the lazy writer
deletes the legacy entry on every v2 write, and no write paths remain
for the legacy maps), so the copy cannot overwrite a newer v2 value; a
defense-in-depth check skips the copy when v2 already holds the key. The
migration is idempotent via `HasMigrationRun`. Wired into the subtensor
pallet `on_runtime_upgrade` hook; spec_version bumped 428 -> 429.
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.

1 participant