Skip to content
Merged
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
7 changes: 5 additions & 2 deletions docs/guides/root-reborn.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -239,8 +239,11 @@ root-stakes to.
deadline and nothing expires.
- **Claim fee.** The inclusion fee scales with how many ALPHA types the
basket holds. Both root-claim calls reserve a conservative 256-unit work
envelope at inclusion, independent of how many networks currently exist,
and refund the unused part after the claim. The amount you actually spend
envelope on every chain and refund the unused part after the claim. Admission
counts only root-relevant validator hotkeys plus their stored basket rows;
unrelated subnet stakes are filtered instead of being multiplied by the total
network count. Classifying the staking-hotkey vector is separately capped at
256 relationships. The amount you actually spend
follows the holdings scanned and redeemed (around τ0.057 on a full
128-holding basket).
`btcli root claim --dry-run` shows reserved versus spent, compares the
Expand Down
7 changes: 5 additions & 2 deletions docs/guides/staking.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -190,8 +190,11 @@ btcli stake remove --netuid 0 --hotkey 5F... --amount all --claim # claim all,
```

The claim fee scales with how many ALPHA types are in the basket. Both
root-claim calls reserve a conservative 256-unit work envelope at inclusion,
independent of the current network count, and refund the unused part after.
root-claim calls reserve a conservative 256-unit work envelope on every chain
and refund the unused part after. Admission counts only root-relevant validator
hotkeys plus their stored basket rows; unrelated subnet stakes are filtered
instead of being multiplied by the total network count. Classifying the
staking-hotkey vector is separately capped at 256 relationships.
You actually spend according to the holdings scanned and redeemed (around
τ0.057 on a full 128-holding basket). `--dry-run` and the confirm step show
reserved versus spent, warn if that spent fee exceeds accrued yield, and
Expand Down
6 changes: 4 additions & 2 deletions pallets/subtensor/src/benchmarks/benchmarks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2051,8 +2051,10 @@ mod pallet_benchmarks {
}

#[benchmark]
fn claim_root(h: Linear<1, { crate::MAX_ROOT_CLAIM_WORK }>) {
// Coldkey-wide claim: `h` validator hotkeys, one holding each. `subnets` is ignored.
fn claim_root(h: Linear<1, { crate::MAX_ROOT_CLAIM_WORK / 2 }>) {
// Coldkey-wide claim: `h` validator hotkeys, one holding each. Each validator consumes
// two admission units (the selected hotkey plus its basket row), so the benchmark's
// executable maximum is half the combined work envelope. `subnets` is ignored.
let coldkey: T::AccountId = whitelisted_caller();
let owner_coldkey: T::AccountId = account("claim_owner_cold", 0, 0);
let owner_hotkey: T::AccountId = account("claim_owner_hot", 0, 1);
Expand Down
6 changes: 2 additions & 4 deletions pallets/subtensor/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,8 @@ pub const MIN_ALPHA_LOW: u16 = 1_639;
pub const MAX_ROOT_CLAIM_THRESHOLD: u64 = 10_000_000;

/// Benchmark upper bound and admission envelope for `claim_root` /
/// `claim_root_scan` (Linear<1, N>). Weight calculation cannot walk storage,
/// so both claim paths reserve this many units and refuse work that would
/// exceed the envelope. Post-dispatch weight is refunded to the work
/// actually performed.
/// `claim_root_scan` (`Linear<1, N>`). Both claim paths reserve this many units
/// and refund unused weight after dispatch.
pub const MAX_ROOT_CLAIM_WORK: u32 = 256;

/// Minimum number of positive destination weights required by `set_root_weights`. Softened
Expand Down
19 changes: 11 additions & 8 deletions pallets/subtensor/src/macros/dispatches.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1966,11 +1966,8 @@ mod dispatches {
/// # Events
/// * `RootClaimed`: On successfully claiming the root emissions for a coldkey.
#[pallet::call_index(121)]
// Signer is not in the call data, so admission uses the conservative
// MAX_ROOT_CLAIM_WORK envelope. Execution refuses a fat coldkey that
// would exceed it — use claim_root_with_hotkey per validator.
#[pallet::weight(
<T as crate::pallet::Config>::WeightInfo::claim_root(Pallet::<T>::root_claim_declared_work())
Pallet::<T>::root_claim_declared_weight()
)]
pub fn claim_root(
origin: OriginFor<T>,
Expand All @@ -1979,7 +1976,13 @@ mod dispatches {
let coldkey: T::AccountId = ensure_signed(origin)?;
let _ = subnets; // ignored: basket claims are fund-level, not per-subnet

let hotkeys = StakingHotkeys::<T>::get(&coldkey);
let staking_hotkeys = StakingHotkeys::<T>::get(&coldkey);
let selection_scanned = u32::try_from(staking_hotkeys.len()).unwrap_or(u32::MAX);
ensure!(
selection_scanned <= Self::root_claim_declared_work(),
Error::<T>::RootClaimTooHeavy
);
let hotkeys = Self::root_claim_hotkeys(&coldkey, staking_hotkeys);
ensure!(
Self::root_claim_fits_declared_budget(&hotkeys),
Error::<T>::RootClaimTooHeavy
Expand All @@ -1988,7 +1991,7 @@ mod dispatches {
let outcome = Self::do_root_claim(coldkey.clone(), hotkeys)?;
Self::maybe_add_coldkey_index(&coldkey);

let weight = Self::root_claim_actual_weight(hotkey_count, &outcome);
let weight = Self::root_claim_actual_weight(hotkey_count, selection_scanned, &outcome);
Ok((Some(weight), Pays::Yes).into())
}

Expand All @@ -2008,7 +2011,7 @@ mod dispatches {
/// * `RootClaimed`: On successfully claiming the root emissions for this coldkey+hotkey.
#[pallet::call_index(148)]
#[pallet::weight(
<T as crate::pallet::Config>::WeightInfo::claim_root(crate::MAX_ROOT_CLAIM_WORK)
Pallet::<T>::root_claim_declared_weight()
)]
pub fn claim_root_with_hotkey(
origin: OriginFor<T>,
Expand All @@ -2023,7 +2026,7 @@ mod dispatches {
let outcome = Self::do_root_claim(coldkey.clone(), vec![hotkey])?;
Self::maybe_add_coldkey_index(&coldkey);

let weight = Self::root_claim_actual_weight(1, &outcome);
let weight = Self::root_claim_actual_weight(1, 0, &outcome);
Ok((Some(weight), Pays::Yes).into())
}

Expand Down
4 changes: 2 additions & 2 deletions pallets/subtensor/src/macros/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -361,8 +361,8 @@ mod errors {
/// (dividends accumulate in place) until weight setting is switched on by
/// governance or a later upgrade.
RootWeightSettingDisabled,
/// Coldkey-wide `claim_root` would process more work units than the
/// pre-dispatch envelope ([`crate::MAX_ROOT_CLAIM_WORK`]). Use
/// A root claim would process more root hotkeys and basket rows than the
/// fixed admission envelope. Use
/// `claim_root_with_hotkey` per validator so admission weight matches
/// the holdings actually walked.
RootClaimTooHeavy,
Expand Down
19 changes: 10 additions & 9 deletions pallets/subtensor/src/macros/hooks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -289,20 +289,21 @@ mod hooks {
);
}

// StakingHotkeys cleanup depends on storage GC having removed zero legacy Alpha rows.
// It is otherwise independent and does not block or gate any runtime operation.
let storage_bloat_cursor_read = T::DbWeight::get().reads(1);
let storage_bloat_in_progress = if !seed_in_progress
// StakingHotkeys cleanup depends on storage GC having completed all of its targets.
// Gate on the positive completion marker rather than cursor absence so an absent or
// unexpectedly removed cursor cannot start the dependent cleanup early.
let storage_bloat_completion_read = T::DbWeight::get().reads(1);
let storage_bloat_complete = if !seed_in_progress
&& weight
.saturating_add(storage_bloat_cursor_read)
.saturating_add(storage_bloat_completion_read)
.all_lt(limit)
{
weight.saturating_accrue(storage_bloat_cursor_read);
migrations::migrate_storage_bloat_v2::StorageBloatCleanupMigration::<T>::exists()
weight.saturating_accrue(storage_bloat_completion_read);
migrations::migrate_storage_bloat_v2::storage_bloat_cleanup_complete::<T>()
} else {
true
false
};
if !seed_in_progress && !storage_bloat_in_progress && weight.all_lt(limit) {
if !seed_in_progress && storage_bloat_complete && weight.all_lt(limit) {
weight.saturating_accrue(
migrations::migrate_cleanup_staking_hotkeys::continue_staking_hotkeys_cleanup::<
T,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ use scale_info::prelude::string::String;
use sp_std::collections::btree_set::BTreeSet;
use sp_std::vec::Vec;

pub const MIGRATION_NAME: &[u8] = b"migrate_cleanup_staking_hotkeys";
// Fresh marker reruns the same bounded cleanup after share-pool dust canonicalization. The
// original `migrate_cleanup_staking_hotkeys` pass may already be marked complete on-chain.
pub const MIGRATION_NAME: &[u8] = b"migrate_cleanup_staking_hotkeys_v2";

/// Persistent progress for the bounded `StakingHotkeys` cleanup.
///
Expand Down Expand Up @@ -48,11 +50,10 @@ fn row_load_weight<T: Config>() -> Weight {

/// A conservative keep predicate for one hotkey/coldkey relationship.
///
/// Any stored share row is retained, including a zero-valued legacy row. The storage-bloat
/// migration runs first and clears zero legacy rows, while treating an unexpected remaining row
/// as live makes this cleanup fail safe. A basket watermark is also sufficient to retain the
/// relationship because zero-root-stake claimants still need to be discoverable by claims and
/// coldkey swaps.
/// Any stored non-zero share row is retained. The storage-bloat migration runs first and clears
/// exact-zero Alpha and AlphaV2 rows; treating an unexpected remaining row as live keeps this
/// cleanup fail safe. A basket watermark is also sufficient to retain the relationship because
/// zero-root-stake claimants still need to be discoverable by claims and coldkey swaps.
fn relationship_must_remain<T: Config>(hotkey: &T::AccountId, coldkey: &T::AccountId) -> bool {
AlphaV2::<T>::iter_prefix((hotkey, coldkey))
.next()
Expand Down
18 changes: 17 additions & 1 deletion pallets/subtensor/src/migrations/migrate_storage_bloat_v2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ use scale_info::prelude::string::String;
use sp_io::{hashing::twox_128, storage};
use sp_std::vec::Vec;

const MIGRATION_NAME: &[u8] = b"migrate_storage_bloat_v2";
// Fresh marker reruns the bounded GC with the AlphaV2 zero-row target added below. The original
// v2 pass may already be marked complete on-chain.
const MIGRATION_NAME: &[u8] = b"migrate_storage_bloat_v3";

#[derive(Clone, Copy, PartialEq, Eq)]
enum CleanupMode {
Expand Down Expand Up @@ -90,6 +92,12 @@ const TARGETS: &[CleanupTarget] = &[
storage: "StakingHotkeys",
mode: CleanupMode::ClearIfZero,
},
// Appended so an interrupted v2 cursor keeps the same target indices after upgrade.
CleanupTarget {
pallet: "SubtensorModule",
storage: "AlphaV2",
mode: CleanupMode::ClearIfZero,
},
];

/// Persistent progress for the bounded storage cleanup.
Expand All @@ -109,6 +117,14 @@ pub struct StorageBloatCleanupProgress {
pub type StorageBloatCleanupMigration<T: Config> =
StorageValue<Pallet<T>, StorageBloatCleanupProgress, OptionQuery>;

/// Returns true only after the current storage-bloat sweep has completed successfully.
///
/// Dependants must use this marker instead of cursor absence: a missing cursor can also mean
/// that the migration was never scheduled or that its progress state was removed unexpectedly.
pub fn storage_bloat_cleanup_complete<T: Config>() -> bool {
HasMigrationRun::<T>::get(MIGRATION_NAME)
}

fn storage_prefix(pallet: &str, item: &str) -> Vec<u8> {
[twox_128(pallet.as_bytes()), twox_128(item.as_bytes())].concat()
}
Expand Down
Loading
Loading