diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index ff470ad3e1..17a56ac8e6 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -2517,6 +2517,24 @@ pub mod pallet { DefaultZeroAlpha, >; + /// --- MAP ( delegator root uid ) --> manager root uid | weight-vector management authorization. + /// + /// A root validator can authorize another root validator (the manager) to set its beta-basket + /// weight vector via `set_root_weights_for`. The manager writes a *bespoke* vector into the + /// delegator's own `Weights[ROOT]` slot, so a single manager can run an independent vector per + /// delegator (not one shared vector everyone copies). This map only records the authorization + /// and routes the manager's `RootWeightTake`; distribution always reads the delegator's own + /// slot. uid-keyed (both sides) so it follows either validator through hotkey swaps with no + /// migration; the fee credit is guarded so a stale/reused uid is never paid. + #[pallet::storage] + pub type RootWeightManager = StorageMap<_, Identity, u16, u16, OptionQuery>; + + /// --- MAP ( manager root uid ) --> take (bps, 0..=10000) | curation fee a manager charges its + /// delegators for setting their basket weight vector. Skimmed from each delegator's root + /// dividend (in TAO) and credited to the manager's own root stake during distribution. + #[pallet::storage] + pub type RootWeightTake = StorageMap<_, Identity, u16, u16, ValueQuery>; + #[pallet::storage] // -- MAP ( cold ) --> root_claim_type enum pub type RootClaimType = StorageMap< _, diff --git a/pallets/subtensor/src/macros/dispatches.rs b/pallets/subtensor/src/macros/dispatches.rs index 1514045c4f..e4e1238b3b 100644 --- a/pallets/subtensor/src/macros/dispatches.rs +++ b/pallets/subtensor/src/macros/dispatches.rs @@ -117,6 +117,56 @@ mod dispatches { Self::do_set_root_weights(origin, dests, weights, version_key) } + /// --- Authorizes `manager` (another root validator) to set this validator's beta-basket + /// weight vector via `set_root_weights_for`, or clears the authorization by passing the + /// caller's own hotkey. The manager also earns its curation take on this validator's basket. + /// + /// # Args: + /// * `origin`: the delegator root validator hotkey. + /// * `manager` (T::AccountId): the manager's root validator hotkey (self = clear). + #[pallet::call_index(140)] + #[pallet::weight((::WeightInfo::set_childkey_take(), DispatchClass::Normal, Pays::Yes))] + pub fn set_root_weight_manager( + origin: OriginFor, + manager: T::AccountId, + ) -> DispatchResult { + Self::do_set_root_weight_manager(origin, manager) + } + + /// --- Sets the curation take (basis points) this root validator charges its + /// weight-vector delegators. Bounded by the childkey-take ceiling; `0` curates for free. + /// + /// # Args: + /// * `origin`: the manager root validator hotkey. + /// * `take` (u16): the take in basis points (0..=10000, capped at the childkey-take max). + #[pallet::call_index(141)] + #[pallet::weight((::WeightInfo::set_childkey_take(), DispatchClass::Normal, Pays::Yes))] + pub fn set_root_weight_take(origin: OriginFor, take: u16) -> DispatchResult { + Self::do_set_root_weight_take(origin, take) + } + + /// --- Sets the beta-basket weight vector of a `delegator` root validator that has + /// authorized the caller as its manager. The vector is written into the delegator's own + /// slot, so one manager can curate an independent vector per delegator. + /// + /// # Args: + /// * `origin`: the manager root validator hotkey. + /// * `delegator` (T::AccountId): the delegator whose vector is being set. + /// * `dests` (Vec): destination subnet netuids. + /// * `weights` (Vec): per-subnet weights (normalized on use). + /// * `version_key` (u64): the network version key. + #[pallet::call_index(142)] + #[pallet::weight((::WeightInfo::set_weights(), DispatchClass::Normal, Pays::No))] + pub fn set_root_weights_for( + origin: OriginFor, + delegator: T::AccountId, + dests: Vec, + weights: Vec, + version_key: u64, + ) -> DispatchResult { + Self::do_set_root_weights_for(origin, delegator, dests, weights, version_key) + } + /// --- Sets the caller weights for the incentive mechanism for mechanisms. The call /// can be made from the hotkey account so is potentially insecure, however, the damage /// of changing weights is minimal if caught early. This function includes all the diff --git a/pallets/subtensor/src/macros/errors.rs b/pallets/subtensor/src/macros/errors.rs index b005d761d8..fb717d82aa 100644 --- a/pallets/subtensor/src/macros/errors.rs +++ b/pallets/subtensor/src/macros/errors.rs @@ -165,6 +165,12 @@ mod errors { NewColdKeyIsHotkey, /// Childkey take is invalid. InvalidChildkeyTake, + /// Root-weight delegation curation take is invalid (exceeds the max). + InvalidRootWeightTake, + /// Root-weight curation take increase rate limit exceeded. + TxRootWeightTakeRateLimitExceeded, + /// Caller is not the authorized weight-vector manager for this delegator. + NotRootWeightManager, /// Childkey take rate limit exceeded. TxChildkeyTakeRateLimitExceeded, /// Invalid identity. diff --git a/pallets/subtensor/src/macros/events.rs b/pallets/subtensor/src/macros/events.rs index a8b1273cb6..192102cba9 100644 --- a/pallets/subtensor/src/macros/events.rs +++ b/pallets/subtensor/src/macros/events.rs @@ -44,6 +44,14 @@ mod events { WeightsSet(NetUidStorageIndex, u16), /// a root validator set its beta-basket distribution vector (uid on the root subnet). RootWeightsSet(u16), + /// a root validator authorized a manager to set its basket weight vector (or cleared it by + /// authorizing itself). Fields: (delegator_uid, manager_uid); equal uids = cleared. + RootWeightManagerSet(u16, u16), + /// a manager set a delegator's basket weight vector on its behalf. Fields: + /// (delegator_uid, manager_uid). + RootWeightsSetByManager(u16, u16), + /// a manager set the curation take it charges delegators. Fields: (manager_uid, take_bps). + RootWeightTakeSet(u16, u16), /// a new neuron account has been registered to the chain. NeuronRegistered(NetUid, u16, T::AccountId), /// multiple uids have been concurrently registered. diff --git a/pallets/subtensor/src/staking/claim_root.rs b/pallets/subtensor/src/staking/claim_root.rs index fdcfc151c2..b9bc0e5b76 100644 --- a/pallets/subtensor/src/staking/claim_root.rs +++ b/pallets/subtensor/src/staking/claim_root.rs @@ -87,13 +87,17 @@ impl Pallet { return; } - // Resolve the validator's basket weight vector w = Weights[ROOT][uid]. The vector follows - // the validator's root uid (so it survives hotkey swaps automatically) and reuses the - // existing root weights plumbing. + // Resolve the validator's OWN basket weight vector w = Weights[ROOT][uid]. If a manager is + // authorized it writes a bespoke vector directly into this slot (see `set_root_weights_for`), + // so distribution reads the slot unconditionally. The vector follows the validator's root + // uid (surviving hotkey swaps) and reuses the existing root weights plumbing. The + // `RootWeightManager` pointer is read only to route the manager's `RootWeightTake` (skimmed + // as a curation fee below). let maybe_uid = Uids::::try_get(NetUid::ROOT, hotkey).ok(); let weights = maybe_uid .map(|uid| Weights::::get(NetUidStorageIndex::ROOT, uid)) .unwrap_or_default(); + let manager_uid: Option = maybe_uid.and_then(|uid| RootWeightManager::::get(uid)); // Keep weights that point at root (uid 0) or an existing subnet. Root is a valid // destination: that slice is held as a root-stake (TAO) basket slot instead of being @@ -145,7 +149,39 @@ impl Pallet { Self::record_protocol_outflow(origin_netuid, tao_total); // 2. Split the TAO across subnets per w and buy each subnet's alpha. - let tao_total_u64: u64 = tao_total.to_u64(); + let mut tao_total_u64: u64 = tao_total.to_u64(); + + // 2a. If an authorized manager curates this validator, skim the manager's curation take + // (bps) from the TAO and credit it to the manager's own root stake. Keeps the round + // trip TotalStake-neutral: sell - buys - fee == 0. The buy loop below redeploys only + // the post-fee remainder. Guarded by `Keys::try_get` so a stale/vacant manager uid is + // never credited (the remainder simply stays with the delegator's basket). + if let Some(m_uid) = manager_uid { + let take: u16 = RootWeightTake::::get(m_uid); + if take > 0 { + if let Ok(manager_hotkey) = Keys::::try_get(NetUid::ROOT, m_uid) { + let fee: u64 = U96F32::saturating_from_num(tao_total_u64) + .saturating_mul(U96F32::saturating_from_num(take)) + .checked_div(U96F32::saturating_from_num(10_000u64)) + .unwrap_or_default() + .saturating_to_num::(); + if fee > 0 { + // Credit the fee as the manager's own root stake (mirrors the root-slot + // and claim paths: stake the TAO, then book the root reserves). + let manager_owner = Owner::::get(&manager_hotkey); + Self::increase_stake_for_hotkey_and_coldkey_on_subnet( + &manager_hotkey, + &manager_owner, + NetUid::ROOT, + fee.into(), + ); + Self::credit_root_reserves(fee.into()); + tao_total_u64 = tao_total_u64.saturating_sub(fee); + } + } + } + } + let mut spent: u64 = 0; let last_idx = valid.len().saturating_sub(1); for (i, (dest_netuid, weight)) in valid.iter().enumerate() { diff --git a/pallets/subtensor/src/subnets/weights.rs b/pallets/subtensor/src/subnets/weights.rs index 515b11185f..bbd5814b53 100644 --- a/pallets/subtensor/src/subnets/weights.rs +++ b/pallets/subtensor/src/subnets/weights.rs @@ -935,52 +935,46 @@ impl Pallet { /// values as the proportion of the validator's root dividends to deploy into each subnet's /// alpha basket. Stored under `Weights[NetUidStorageIndex::ROOT][uid]` and consumed by /// `distribute_root_alpha_to_basket` during emission. - pub fn do_set_root_weights( - origin: OriginFor, + /// Shared inner: validate and store a beta-basket weight vector into `Weights[ROOT][target_uid]` + /// for `target_hotkey` (the slot owner). Used by both the self path (`do_set_root_weights`) and + /// the manager-on-behalf path (`do_set_root_weights_for`); each does its own + /// identity/authorization check first, then calls this with the resolved slot owner. + fn apply_root_weights( + target_hotkey: &T::AccountId, + target_uid: u16, dests: Vec, values: Vec, version_key: u64, ) -> dispatch::DispatchResult { - // --- 1. Signed by the root validator hotkey. - let hotkey = ensure_signed(origin)?; - log::debug!("do_set_root_weights( hotkey:{hotkey:?}, dests:{dests:?}, values:{values:?} )"); - - // --- 2. Lengths match. + // --- Lengths match. ensure!( Self::uids_match_values(&dests, &values), Error::::WeightVecNotEqualSize ); - // --- 3. Caller must be a registered root validator. - ensure!( - Self::is_hotkey_registered_on_network(NetUid::ROOT, &hotkey), - Error::::HotKeyNotRegisteredInSubNet - ); - - // --- 4. Must hold enough stake to set weights. + // --- Slot owner must hold enough stake to carry a weighted basket. ensure!( - Self::check_weights_min_stake(&hotkey, NetUid::ROOT), + Self::check_weights_min_stake(target_hotkey, NetUid::ROOT), Error::::NotEnoughStakeToSetWeights ); - // --- 5. Version key must be current. + // --- Version key must be current. ensure!( Self::check_version_key(NetUid::ROOT, version_key), Error::::IncorrectWeightVersionKey ); - // --- 6. Rate limit on the root weights index. - let neuron_uid = Self::get_uid_for_net_and_hotkey(NetUid::ROOT, &hotkey)?; + // --- Rate limit on the slot owner's root weights index. let current_block: u64 = Self::get_current_block_as_u64(); ensure!( - Self::check_rate_limit(NetUidStorageIndex::ROOT, neuron_uid, current_block), + Self::check_rate_limit(NetUidStorageIndex::ROOT, target_uid, current_block), Error::::SettingWeightsTooFast ); - // --- 7. No duplicate destination subnets. + // --- No duplicate destination subnets. ensure!(!Self::has_duplicate_uids(&dests), Error::::DuplicateUids); - // --- 8. Every destination must be root (uid 0) or an existing subnet. Root is a valid + // --- Every destination must be root (uid 0) or an existing subnet. Root is a valid // basket destination: that weight slice is held as root stake (TAO) instead of being // deployed into a subnet, letting a validator opt out of subnet exposure. This must mirror // the consumer filter in `distribute_root_alpha_to_basket`. @@ -992,23 +986,130 @@ impl Pallet { ); } - // --- 9. Max-upscale the weights. + // --- Max-upscale and store under the root weights index (reusing the root plumbing). let max_upscaled_weights: Vec = vec_u16_max_upscale_to_u16(&values); - - // --- 10. Zip and store under the root weights index (reusing the root weights plumbing). let zipped_weights: Vec<(u16, u16)> = dests .iter() .copied() .zip(max_upscaled_weights.iter().copied()) .collect(); - Weights::::insert(NetUidStorageIndex::ROOT, neuron_uid, zipped_weights); + Weights::::insert(NetUidStorageIndex::ROOT, target_uid, zipped_weights); + + // --- Record activity for the rate limit and emit. + Self::set_last_update_for_uid(NetUidStorageIndex::ROOT, target_uid, current_block); + log::debug!("RootWeightsSet( uid:{target_uid:?} )"); + Self::deposit_event(Event::RootWeightsSet(target_uid)); + + Ok(()) + } + + /// Sets the caller's own beta-basket weight vector on the root subnet (netuid 0). + pub fn do_set_root_weights( + origin: OriginFor, + dests: Vec, + values: Vec, + version_key: u64, + ) -> dispatch::DispatchResult { + // --- Signed by, and registered as, a root validator. + let hotkey = ensure_signed(origin)?; + log::debug!("do_set_root_weights( hotkey:{hotkey:?}, dests:{dests:?}, values:{values:?} )"); + ensure!( + Self::is_hotkey_registered_on_network(NetUid::ROOT, &hotkey), + Error::::HotKeyNotRegisteredInSubNet + ); + let neuron_uid = Self::get_uid_for_net_and_hotkey(NetUid::ROOT, &hotkey)?; + + Self::apply_root_weights(&hotkey, neuron_uid, dests, values, version_key) + } + + /// Sets the beta-basket weight vector of a `delegator` that has authorized the caller as its + /// manager (via `set_root_weight_manager`). The vector is written into the *delegator's own* + /// slot, so one manager can run an independent vector per delegator. The manager earns its + /// `RootWeightTake` on the delegator's distributions. + pub fn do_set_root_weights_for( + origin: OriginFor, + delegator: T::AccountId, + dests: Vec, + values: Vec, + version_key: u64, + ) -> dispatch::DispatchResult { + // --- Signed by a registered root validator (the manager). + let manager = ensure_signed(origin)?; + let manager_uid = Self::get_uid_for_net_and_hotkey(NetUid::ROOT, &manager)?; + + // --- The delegator must have authorized this manager. + let delegator_uid = Self::get_uid_for_net_and_hotkey(NetUid::ROOT, &delegator)?; + ensure!( + RootWeightManager::::get(delegator_uid) == Some(manager_uid), + Error::::NotRootWeightManager + ); - // --- 11. Record activity for the rate limit. - Self::set_last_update_for_uid(NetUidStorageIndex::ROOT, neuron_uid, current_block); + Self::apply_root_weights(&delegator, delegator_uid, dests, values, version_key)?; + Self::deposit_event(Event::RootWeightsSetByManager(delegator_uid, manager_uid)); + Ok(()) + } - // --- 12. Emit event. - log::debug!("RootWeightsSet( uid:{neuron_uid:?} )"); - Self::deposit_event(Event::RootWeightsSet(neuron_uid)); + /// Authorizes (or clears) a `manager` root validator to set the caller's beta-basket weight + /// vector via `set_root_weights_for`. Authorizing oneself clears the authorization + /// (self-manage). The manager additionally earns its `RootWeightTake` on the caller's + /// distributions. + pub fn do_set_root_weight_manager( + origin: OriginFor, + manager: T::AccountId, + ) -> dispatch::DispatchResult { + // --- Signed by the delegator root validator hotkey. + let hotkey = ensure_signed(origin)?; + let delegator_uid = Self::get_uid_for_net_and_hotkey(NetUid::ROOT, &hotkey)?; + + // --- Authorizing oneself clears the manager; otherwise the manager must also be a + // registered root validator. + if manager == hotkey { + RootWeightManager::::remove(delegator_uid); + Self::deposit_event(Event::RootWeightManagerSet(delegator_uid, delegator_uid)); + } else { + let manager_uid = Self::get_uid_for_net_and_hotkey(NetUid::ROOT, &manager)?; + RootWeightManager::::insert(delegator_uid, manager_uid); + Self::deposit_event(Event::RootWeightManagerSet(delegator_uid, manager_uid)); + } + + Ok(()) + } + + /// Sets the curation take (basis points, `0..=MaxChildkeyTake`) a manager root validator + /// charges its delegators. `0` lets a manager curate for free. Bounded by the same ceiling as + /// childkey take. + pub fn do_set_root_weight_take(origin: OriginFor, take: u16) -> dispatch::DispatchResult { + // --- 1. Signed by the manager root validator hotkey. + let hotkey = ensure_signed(origin)?; + + // --- 2. Manager must be a registered root validator. + let manager_uid = Self::get_uid_for_net_and_hotkey(NetUid::ROOT, &hotkey)?; + + // --- 3. Take must be within the allowed ceiling. + ensure!( + take <= Self::get_max_childkey_take(), + Error::::InvalidRootWeightTake + ); + + // --- 4. Rate-limit *increases* only (same window as childkey take), so a manager cannot + // spike its take on already-committed delegators between their per-block distributions. + // Lowering the take is always allowed. + let current_block = Self::get_current_block_as_u64(); + if take > RootWeightTake::::get(manager_uid) { + ensure!( + TransactionType::SetRootWeightTake + .passes_rate_limit_on_subnet::(&hotkey, NetUid::ROOT), + Error::::TxRootWeightTakeRateLimitExceeded + ); + } + + RootWeightTake::::insert(manager_uid, take); + TransactionType::SetRootWeightTake.set_last_block_on_subnet::( + &hotkey, + NetUid::ROOT, + current_block, + ); + Self::deposit_event(Event::RootWeightTakeSet(manager_uid, take)); Ok(()) } diff --git a/pallets/subtensor/src/tests/claim_root.rs b/pallets/subtensor/src/tests/claim_root.rs index c71a174238..2159384ddd 100644 --- a/pallets/subtensor/src/tests/claim_root.rs +++ b/pallets/subtensor/src/tests/claim_root.rs @@ -4,9 +4,9 @@ use crate::tests::mock::*; use crate::{ BasketPrincipal, DefaultMinRootClaimAmount, Error, Keys, MAX_NUM_ROOT_CLAIMS, MAX_ROOT_CLAIM_THRESHOLD, NetworksAdded, NumRootClaim, NumStakingColdkeys, RootClaimType, - RootClaimTypeEnum, RootClaimable, RootClaimableThreshold, RootClaimed, StakingColdkeys, - StakingColdkeysByIndex, SubnetAlphaIn, SubnetMovingPrice, SubnetProtocolFlow, SubnetTAO, - SubnetworkN, Tempo, TotalStake, Uids, Weights, + RootClaimTypeEnum, RootClaimable, RootClaimableThreshold, RootClaimed, RootWeightManager, + RootWeightTake, StakingColdkeys, StakingColdkeysByIndex, SubnetAlphaIn, SubnetMovingPrice, + SubnetProtocolFlow, SubnetTAO, SubnetworkN, Tempo, TotalStake, Uids, Weights, }; use approx::assert_abs_diff_eq; use frame_support::dispatch::RawOrigin; @@ -517,6 +517,219 @@ fn test_root_basket_records_symmetric_protocol_flow() { }); } +/// Registers `hotkey` as root validator `uid` (Uids + Keys), enough for the root-weight +/// extrinsics' identity/rate-limit checks. +fn register_root(hotkey: &U256, uid: u16) { + Uids::::insert(NetUid::ROOT, hotkey, uid); + Keys::::insert(NetUid::ROOT, uid, *hotkey); +} + +/// End-to-end via the extrinsics: a delegator authorizes a manager, the manager sets the +/// delegator's *own* vector on its behalf (rejected before authorization), and the delegator's +/// dividend then deploys per that vector with the manager's take skimmed to the manager's root +/// stake. +#[test] +fn test_root_weight_manager_sets_delegator_vector_and_earns_take() { + new_test_ext(1).execute_with(|| { + let owner_m = U256::from(1001); + let hotkey_m = U256::from(1002); + let owner_a = U256::from(2001); + let hotkey_a = U256::from(2002); + let coldkey_a = U256::from(2003); + let owner_t = U256::from(3001); + let hotkey_t = U256::from(3002); + + // netuid_a = delegator's dividend origin; netuid_t = the manager's chosen basket subnet. + let netuid_a = add_dynamic_network(&hotkey_a, &owner_a); + let netuid_t = add_dynamic_network(&hotkey_t, &owner_t); + add_dynamic_network(&hotkey_m, &owner_m); // Owner[M] for the fee credit + remove_owner_registration_stake(netuid_a); + fund_pool(netuid_a); + fund_pool(netuid_t); + + SubtensorModule::set_tao_weight(u64::MAX); + RootClaimableThreshold::::insert(netuid_t, I96F32::from_num(0)); + + // Root subnet must exist for the weight extrinsics' rate-limit resolution. + add_network(NetUid::ROOT, 1, 0); + register_root(&hotkey_m, 0); + register_root(&hotkey_a, 1); + + // Manager sets its take. + assert_ok!(SubtensorModule::set_root_weight_take( + RuntimeOrigin::signed(hotkey_m), + 1000 // 10% + )); + + // Before authorization the manager cannot set the delegator's vector. + assert_noop!( + SubtensorModule::set_root_weights_for( + RuntimeOrigin::signed(hotkey_m), + hotkey_a, + vec![u16::from(netuid_t)], + vec![u16::MAX], + 0, + ), + Error::::NotRootWeightManager + ); + + // Delegator authorizes the manager; the manager then sets the delegator's OWN slot. + assert_ok!(SubtensorModule::set_root_weight_manager( + RuntimeOrigin::signed(hotkey_a), + hotkey_m + )); + assert_eq!(RootWeightManager::::get(1u16), Some(0u16)); + assert_ok!(SubtensorModule::set_root_weights_for( + RuntimeOrigin::signed(hotkey_m), + hotkey_a, + vec![u16::from(netuid_t)], + vec![u16::MAX], + 0, + )); + // The vector landed in the DELEGATOR's slot (uid 1), not the manager's. + assert!(!Weights::::get(NetUidStorageIndex::ROOT, 1u16).is_empty()); + assert!(Weights::::get(NetUidStorageIndex::ROOT, 0u16).is_empty()); + + // Delegator needs root stake to apportion against, and alpha on A to earn the dividend. + mock_increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_a, + &coldkey_a, + NetUid::ROOT, + 2_000_000u64.into(), + ); + mock_increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_a, + &owner_a, + netuid_a, + 10_000_000u64.into(), + ); + + assert_eq!(root_stake_of(&hotkey_m, &owner_m), 0); + + SubtensorModule::distribute_emission( + netuid_a, + AlphaBalance::ZERO, + AlphaBalance::ZERO, + 1_000_000u64.into(), + AlphaBalance::ZERO, + ); + + // Basket built on the manager-chosen subnet; manager earned the take as root stake. + let basket = escrow_alpha(&hotkey_a, netuid_t); + let fee = root_stake_of(&hotkey_m, &owner_m); + assert!(basket > 0, "delegator basket should follow the manager-set vector"); + assert!(fee > 0, "manager should receive the curation take"); + // 10% take => basket (~90% of gross at pool price ~1) is ~9x the fee. + assert_abs_diff_eq!(basket as i64, fee as i64 * 9, epsilon = (fee as i64) / 20); + }); +} + +/// The core requirement: one manager runs *independent* vectors for two delegators (A -> X, +/// B -> Y), not one shared vector both copy. Each delegator's dividend deploys into its own +/// manager-set subnet, and the manager earns a take on each. +#[test] +fn test_root_weight_manager_runs_independent_vectors() { + new_test_ext(1).execute_with(|| { + let owner_m = U256::from(1001); + let hotkey_m = U256::from(1002); + let (owner_a, hotkey_a, coldkey_a) = (U256::from(2001), U256::from(2002), U256::from(2003)); + let (owner_b, hotkey_b, coldkey_b) = (U256::from(3001), U256::from(3002), U256::from(3003)); + let (owner_x, hotkey_x) = (U256::from(4001), U256::from(4002)); + let (owner_y, hotkey_y) = (U256::from(5001), U256::from(5002)); + + // Origins where A and B earn dividends; X and Y are the two distinct basket targets. + let netuid_a = add_dynamic_network(&hotkey_a, &owner_a); + let netuid_b = add_dynamic_network(&hotkey_b, &owner_b); + let netuid_x = add_dynamic_network(&hotkey_x, &owner_x); + let netuid_y = add_dynamic_network(&hotkey_y, &owner_y); + add_dynamic_network(&hotkey_m, &owner_m); // Owner[M] for the fee credit + remove_owner_registration_stake(netuid_a); + remove_owner_registration_stake(netuid_b); + for n in [netuid_a, netuid_b, netuid_x, netuid_y] { + fund_pool(n); + } + SubtensorModule::set_tao_weight(u64::MAX); + RootClaimableThreshold::::insert(netuid_x, I96F32::from_num(0)); + RootClaimableThreshold::::insert(netuid_y, I96F32::from_num(0)); + + // Root subnet must exist for the weight extrinsics' rate-limit resolution. + add_network(NetUid::ROOT, 1, 0); + register_root(&hotkey_m, 0); + register_root(&hotkey_a, 1); + register_root(&hotkey_b, 2); + RootWeightTake::::insert(0u16, 1000u16); // manager charges 10% + + // Both delegators authorize the manager; the manager sets DIFFERENT vectors for each. + assert_ok!(SubtensorModule::set_root_weight_manager( + RuntimeOrigin::signed(hotkey_a), + hotkey_m + )); + assert_ok!(SubtensorModule::set_root_weight_manager( + RuntimeOrigin::signed(hotkey_b), + hotkey_m + )); + assert_ok!(SubtensorModule::set_root_weights_for( + RuntimeOrigin::signed(hotkey_m), + hotkey_a, + vec![u16::from(netuid_x)], + vec![u16::MAX], + 0, + )); + assert_ok!(SubtensorModule::set_root_weights_for( + RuntimeOrigin::signed(hotkey_m), + hotkey_b, + vec![u16::from(netuid_y)], + vec![u16::MAX], + 0, + )); + + for (hk, ck, owner, origin) in [ + (hotkey_a, coldkey_a, owner_a, netuid_a), + (hotkey_b, coldkey_b, owner_b, netuid_b), + ] { + mock_increase_stake_for_hotkey_and_coldkey_on_subnet( + &hk, + &ck, + NetUid::ROOT, + 2_000_000u64.into(), + ); + mock_increase_stake_for_hotkey_and_coldkey_on_subnet( + &hk, + &owner, + origin, + 10_000_000u64.into(), + ); + } + + SubtensorModule::distribute_emission( + netuid_a, + AlphaBalance::ZERO, + AlphaBalance::ZERO, + 1_000_000u64.into(), + AlphaBalance::ZERO, + ); + let fee_after_a = root_stake_of(&hotkey_m, &owner_m); + SubtensorModule::distribute_emission( + netuid_b, + AlphaBalance::ZERO, + AlphaBalance::ZERO, + 1_000_000u64.into(), + AlphaBalance::ZERO, + ); + let fee_after_b = root_stake_of(&hotkey_m, &owner_m); + + // Independent baskets: A lands only on X, B lands only on Y. + assert!(escrow_alpha(&hotkey_a, netuid_x) > 0, "A should hold X"); + assert_eq!(escrow_alpha(&hotkey_a, netuid_y), 0, "A must not hold Y"); + assert!(escrow_alpha(&hotkey_b, netuid_y) > 0, "B should hold Y"); + assert_eq!(escrow_alpha(&hotkey_b, netuid_x), 0, "B must not hold X"); + + // Manager earned a take on each delegator's distribution. + assert!(fee_after_a > 0, "manager earns take on A"); + assert!(fee_after_b > fee_after_a, "manager earns take on B too"); + }); +} + // ============================================================================= // Beta basket: claiming (always full swap to root TAO) // ============================================================================= diff --git a/pallets/subtensor/src/utils/rate_limiting.rs b/pallets/subtensor/src/utils/rate_limiting.rs index e9559f2c6d..25cf3455c5 100644 --- a/pallets/subtensor/src/utils/rate_limiting.rs +++ b/pallets/subtensor/src/utils/rate_limiting.rs @@ -17,6 +17,7 @@ pub enum TransactionType { MechanismEmission, MaxUidsTrimming, AddStakeBurn, + SetRootWeightTake, } impl TransactionType { @@ -25,6 +26,8 @@ impl TransactionType { match self { Self::SetChildren => 150, // 30 minutes Self::SetChildkeyTake => TxChildkeyTakeRateLimit::::get(), + // Root-weight curation take reuses the childkey-take rate window. + Self::SetRootWeightTake => TxChildkeyTakeRateLimit::::get(), Self::RegisterNetwork => NetworkRateLimit::::get(), Self::MechanismCountUpdate => MechanismCountSetRateLimit::::get(), Self::MechanismEmission => MechanismEmissionRateLimit::::get(), @@ -144,6 +147,7 @@ impl From for u16 { TransactionType::MechanismEmission => 8, TransactionType::MaxUidsTrimming => 9, TransactionType::AddStakeBurn => 10, + TransactionType::SetRootWeightTake => 11, } } } @@ -162,6 +166,7 @@ impl From for TransactionType { 8 => TransactionType::MechanismEmission, 9 => TransactionType::MaxUidsTrimming, 10 => TransactionType::AddStakeBurn, + 11 => TransactionType::SetRootWeightTake, _ => TransactionType::Unknown, } }