Skip to content
117 changes: 115 additions & 2 deletions pallets/subtensor/rpc/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ use subtensor_runtime_common::{MechId, NetUid, TaoBalance};
use sp_api::ProvideRuntimeApi;

pub use subtensor_custom_rpc_runtime_api::{
DelegateInfoRuntimeApi, NeuronInfoRuntimeApi, StakeInfoRuntimeApi, SubnetInfoRuntimeApi,
SubnetRegistrationRuntimeApi,
BetaBasketRuntimeApi, DelegateInfoRuntimeApi, NeuronInfoRuntimeApi, StakeInfoRuntimeApi,
SubnetInfoRuntimeApi, SubnetRegistrationRuntimeApi,
};

#[rpc(client, server)]
Expand Down Expand Up @@ -118,6 +118,38 @@ pub trait SubtensorCustomApi<BlockHash> {
netuid: NetUid,
at: Option<BlockHash>,
) -> RpcResult<Vec<u8>>;

/// Total TAO a staker (coldkey) would realize by redeeming all its root beta baskets.
#[method(name = "betaBasket_getStakerOwed")]
fn get_root_basket_owed(
&self,
coldkey: AccountId32,
at: Option<BlockHash>,
) -> RpcResult<TaoBalance>;
/// A validator's beta basket net asset value, in TAO.
#[method(name = "betaBasket_getValidatorNav")]
fn get_validator_basket_nav(
&self,
hotkey: AccountId32,
at: Option<BlockHash>,
) -> RpcResult<TaoBalance>;
/// A validator's full basket breakdown: SCALE-encoded `Vec<(NetUid, AlphaBalance, TaoBalance)>`.
#[method(name = "betaBasket_getValidatorBasket")]
fn get_validator_basket(
&self,
hotkey: AccountId32,
at: Option<BlockHash>,
) -> RpcResult<Vec<u8>>;
/// Network-wide total beta basket NAV across all validators, in TAO.
#[method(name = "betaBasket_getTotalNav")]
fn get_root_basket_total_nav(&self, at: Option<BlockHash>) -> RpcResult<TaoBalance>;
/// A validator's basket weight vector: SCALE-encoded `Vec<(NetUid, u16)>` (its strategy).
#[method(name = "betaBasket_getValidatorWeights")]
fn get_validator_weights(
&self,
hotkey: AccountId32,
at: Option<BlockHash>,
) -> RpcResult<Vec<u8>>;
}

pub struct SubtensorCustom<C, P> {
Expand Down Expand Up @@ -167,6 +199,7 @@ where
C::Api: SubnetInfoRuntimeApi<Block>,
C::Api: StakeInfoRuntimeApi<Block>,
C::Api: SubnetRegistrationRuntimeApi<Block>,
C::Api: BetaBasketRuntimeApi<Block>,
{
fn get_delegates(&self, at: Option<<Block as BlockT>::Hash>) -> RpcResult<Vec<u8>> {
let api = self.client.runtime_api();
Expand Down Expand Up @@ -572,4 +605,84 @@ where
Err(e) => Err(Error::RuntimeError(format!("Unable to get coldkey lock: {e:?}")).into()),
}
}

fn get_root_basket_owed(
&self,
coldkey: AccountId32,
at: Option<<Block as BlockT>::Hash>,
) -> RpcResult<TaoBalance> {
let api = self.client.runtime_api();
let at = at.unwrap_or_else(|| self.client.info().best_hash);

match api.get_root_basket_owed(at, coldkey) {
Ok(result) => Ok(result),
Err(e) => {
Err(Error::RuntimeError(format!("Unable to get root basket owed: {e:?}")).into())
}
}
}

fn get_validator_basket_nav(
&self,
hotkey: AccountId32,
at: Option<<Block as BlockT>::Hash>,
) -> RpcResult<TaoBalance> {
let api = self.client.runtime_api();
let at = at.unwrap_or_else(|| self.client.info().best_hash);

match api.get_validator_basket_nav(at, hotkey) {
Ok(result) => Ok(result),
Err(e) => Err(Error::RuntimeError(format!(
"Unable to get validator basket NAV: {e:?}"
))
.into()),
}
}

fn get_validator_basket(
&self,
hotkey: AccountId32,
at: Option<<Block as BlockT>::Hash>,
) -> RpcResult<Vec<u8>> {
let api = self.client.runtime_api();
let at = at.unwrap_or_else(|| self.client.info().best_hash);

match api.get_validator_basket(at, hotkey) {
Ok(result) => Ok(result.encode()),
Err(e) => {
Err(Error::RuntimeError(format!("Unable to get validator basket: {e:?}")).into())
}
}
}

fn get_root_basket_total_nav(
&self,
at: Option<<Block as BlockT>::Hash>,
) -> RpcResult<TaoBalance> {
let api = self.client.runtime_api();
let at = at.unwrap_or_else(|| self.client.info().best_hash);

match api.get_root_basket_total_nav(at) {
Ok(result) => Ok(result),
Err(e) => {
Err(Error::RuntimeError(format!("Unable to get total basket NAV: {e:?}")).into())
}
}
}

fn get_validator_weights(
&self,
hotkey: AccountId32,
at: Option<<Block as BlockT>::Hash>,
) -> RpcResult<Vec<u8>> {
let api = self.client.runtime_api();
let at = at.unwrap_or_else(|| self.client.info().best_hash);

match api.get_validator_weights(at, hotkey) {
Ok(result) => Ok(result.encode()),
Err(e) => {
Err(Error::RuntimeError(format!("Unable to get validator weights: {e:?}")).into())
}
}
}
}
13 changes: 13 additions & 0 deletions pallets/subtensor/runtime-api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,4 +81,17 @@ sp_api::decl_runtime_apis! {
fn get_proxy_types() -> Vec<ProxyTypeInfo>;
fn get_proxy_filter(proxy_type: Option<u8>) -> Vec<ProxyFilterInfo>;
}

pub trait BetaBasketRuntimeApi {
/// Total TAO a coldkey would realize by redeeming all its root beta baskets (marked).
fn get_root_basket_owed(coldkey: AccountId32) -> TaoBalance;
/// A validator's beta basket net asset value, in TAO (marked).
fn get_validator_basket_nav(hotkey: AccountId32) -> TaoBalance;
/// A validator's basket breakdown: (subnet, alpha held, TAO value) per subnet.
fn get_validator_basket(hotkey: AccountId32) -> Vec<(NetUid, AlphaBalance, TaoBalance)>;
/// Network-wide total beta basket NAV across all validators, in TAO (marked).
fn get_root_basket_total_nav() -> TaoBalance;
/// A validator's basket weight vector `w`: (subnet, weight) it deploys dividends into.
fn get_validator_weights(hotkey: AccountId32) -> Vec<(NetUid, u16)>;
}
}
4 changes: 2 additions & 2 deletions pallets/subtensor/src/benchmarks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1912,7 +1912,7 @@ mod pallet_benchmarks {
let coldkey: T::AccountId = whitelisted_caller();

#[extrinsic_call]
_(RawOrigin::Signed(coldkey.clone()), RootClaimTypeEnum::Keep);
_(RawOrigin::Signed(coldkey.clone()), RootClaimTypeEnum::Swap);
}

#[benchmark]
Expand Down Expand Up @@ -1971,7 +1971,7 @@ mod pallet_benchmarks {

assert_ok!(Subtensor::<T>::set_root_claim_type(
RawOrigin::Signed(coldkey.clone()).into(),
RootClaimTypeEnum::Keep
RootClaimTypeEnum::Swap
));

#[extrinsic_call]
Expand Down
4 changes: 1 addition & 3 deletions pallets/subtensor/src/coinbase/block_step.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ impl<T: Config + pallet_drand::Config> Pallet<T> {
/// Executes the necessary operations for each block.
pub fn block_step() -> Result<(), &'static str> {
let block_number: u64 = Self::get_current_block_as_u64();
let last_block_hash: T::Hash = <frame_system::Pallet<T>>::parent_hash();

// --- 1. Update registration burn prices.
Self::update_registration_prices_for_networks();
Expand All @@ -25,8 +24,7 @@ impl<T: Config + pallet_drand::Config> Pallet<T> {
Self::update_root_prop();
// --- 7. Set pending children on the epoch; but only after the coinbase has been run.
Self::try_set_pending_children(block_number);
// --- 8. Run auto-claim root divs.
Self::run_auto_claim_root_divs(last_block_hash);
// --- 8. Beta baskets are redeemed on-demand by stakers via `claim_root`; no auto-swap.
// --- 9. Populate root coldkey maps.
Self::populate_root_coldkey_staking_maps();
Self::populate_root_coldkey_staking_maps_v2();
Expand Down
10 changes: 5 additions & 5 deletions pallets/subtensor/src/coinbase/run_coinbase.rs
Original file line number Diff line number Diff line change
Expand Up @@ -705,11 +705,11 @@ impl<T: Config> Pallet<T> {
tou64!(alpha_take).into(),
);

Self::increase_root_claimable_for_hotkey_and_subnet(
&hotkey,
netuid,
tou64!(root_alpha).into(),
);
// Distribute the validator's root dividend into its beta basket across subnets
// per the validator's root weight vector (set on subnet 0). The bought basket
// alpha is staked to the validator under the global escrow coldkey, so it counts
// toward the validator's stake and compounds; stakers accrue a claimable rate.
Self::distribute_root_alpha_to_basket(&hotkey, netuid, tou64!(root_alpha).into());

// Record root alpha dividends for this validator on this subnet.
RootAlphaDividendsPerSubnet::<T>::mutate(netuid, &hotkey, |divs| {
Expand Down
33 changes: 31 additions & 2 deletions pallets/subtensor/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -337,13 +337,19 @@ pub mod pallet {
Encode, Decode, Default, TypeInfo, Clone, PartialEq, Eq, Debug, DecodeWithMemTracking,
)]
/// Enum for the per-coldkey root claim setting.
///
/// With beta baskets, redemption is always a full swap to root TAO, so `Swap` is the only
/// supported variant. `Keep` and `KeepSubnets` are deprecated no-ops kept solely for
/// storage/SCALE decode compatibility with values written before the basket model; they are
/// rejected by `set_root_claim_type` and ignored by the claim path.
pub enum RootClaimTypeEnum {
/// Swap any alpha emission for TAO.
#[default]
Swap,
/// Keep all alpha emission.
/// Deprecated no-op (formerly: keep all alpha emission). Rejected by `set_root_claim_type`.
Keep,
/// Keep all alpha emission for specified subnets.
/// Deprecated no-op (formerly: keep alpha emission for specified subnets). Rejected by
/// `set_root_claim_type`.
KeepSubnets {
/// Subnets to keep alpha emissions (swap everything else).
subnets: BTreeSet<NetUid>,
Expand Down Expand Up @@ -2488,6 +2494,29 @@ pub mod pallet {
u128,
ValueQuery,
>;

/// --- DMAP ( validator_hotkey, netuid ) --> outstanding basket principal *shares*.
///
/// Total un-claimed principal shares root stakers hold in this validator's beta basket on
/// `netuid`. The actual basket alpha is staked to the validator under the global beta escrow
/// coldkey (value `E`) and grows with dividends; the per-staker payout at claim time is
/// `owed_shares * (E / BasketPrincipal)`, which captures that compounding. Deposits mint
/// shares at the live NAV (`E/P`), not at par, so a deposit into an already-compounded basket
/// leaves `E/P` unchanged — existing holders are not diluted and late stakers cannot skim
/// past compounding. At a flat NAV (`E == P`, e.g. right after the seed migration) one share
/// equals one alpha, so this also migrates cleanly by value on hotkey swap.
#[pallet::storage]
pub type BasketPrincipal<T: Config> = StorageDoubleMap<
_,
Blake2_128Concat,
T::AccountId,
Identity,
NetUid,
AlphaBalance,
ValueQuery,
DefaultZeroAlpha<T>,
>;

#[pallet::storage] // -- MAP ( cold ) --> root_claim_type enum
pub type RootClaimType<T: Config> = StorageMap<
_,
Expand Down
33 changes: 30 additions & 3 deletions pallets/subtensor/src/macros/dispatches.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,26 @@ mod dispatches {
}
}

/// --- Sets a root validator's beta-basket distribution vector `w` on the root subnet
/// (netuid 0). `dests` are subnet netuids and `weights` are the proportions of the
/// validator's root dividends to deploy into each subnet's alpha basket.
///
/// # Args:
/// * `origin`: the root validator hotkey.
/// * `dests` (Vec<u16>): destination subnet netuids.
/// * `weights` (Vec<u16>): per-subnet weights (normalized on use).
/// * `version_key` (u64): the network version key.
#[pallet::call_index(139)]
#[pallet::weight((<T as crate::pallet::Config>::WeightInfo::set_weights(), DispatchClass::Normal, Pays::No))]
pub fn set_root_weights(
origin: OriginFor<T>,
dests: Vec<u16>,
weights: Vec<u16>,
version_key: u64,
) -> DispatchResult {
Self::do_set_root_weights(origin, 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
Expand Down Expand Up @@ -2182,6 +2202,12 @@ mod dispatches {
}

/// --- Sets the root claim type for the coldkey.
///
/// Beta-basket redemption is always a full swap to root TAO, so only
/// [`RootClaimTypeEnum::Swap`] is accepted. The `Keep` / `KeepSubnets` variants are
/// deprecated no-ops retained only for storage/SCALE decode compatibility and are
/// rejected here so a caller can never set a claim type that silently does nothing.
///
/// # Args:
/// * 'origin': (<T as frame_system::Config>Origin):
/// - The signature of the caller's coldkey.
Expand All @@ -2198,9 +2224,10 @@ mod dispatches {
) -> DispatchResult {
let coldkey: T::AccountId = ensure_signed(origin)?;

if let RootClaimTypeEnum::KeepSubnets { subnets } = &new_root_claim_type {
ensure!(!subnets.is_empty(), Error::<T>::InvalidSubnetNumber);
}
ensure!(
matches!(new_root_claim_type, RootClaimTypeEnum::Swap),
Error::<T>::RootClaimTypeNotSupported
);

Self::maybe_add_coldkey_index(&coldkey);

Expand Down
3 changes: 3 additions & 0 deletions pallets/subtensor/src/macros/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,9 @@ mod errors {
ChildParentInconsistency,
/// Invalid number of root claims
InvalidNumRootClaim,
/// The requested root claim type is no longer supported (only `Swap` is accepted; the
/// `Keep`/`KeepSubnets` variants are deprecated no-ops).
RootClaimTypeNotSupported,
/// Invalid value of root claim threshold
InvalidRootClaimThreshold,
/// Exceeded subnet limit number or zero.
Expand Down
39 changes: 39 additions & 0 deletions pallets/subtensor/src/macros/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ mod events {
),
/// a caller successfully sets their weights on a subnetwork.
WeightsSet(NetUidStorageIndex, u16),
/// a root validator set its beta-basket distribution vector (uid on the root subnet).
RootWeightsSet(u16),
/// a new neuron account has been registered to the chain.
NeuronRegistered(NetUid, u16, T::AccountId),
/// multiple uids have been concurrently registered.
Expand Down Expand Up @@ -481,6 +483,43 @@ mod events {
root_claim_type: RootClaimTypeEnum,
},

/// A validator's beta basket received a deposit on a subnet: `alpha` was bought and staked
/// into the basket, minting `shares` of basket principal at the current NAV.
BasketDeposited {
/// Validator hotkey whose basket received the deposit.
hotkey: T::AccountId,
/// Subnet the basket alpha was bought on.
netuid: NetUid,
/// Alpha bought and staked into the basket (grows escrow value `E`).
alpha: AlphaBalance,
/// Basket principal shares minted at the live NAV (grows `BasketPrincipal`).
shares: AlphaBalance,
},

/// A staker redeemed (claimed) part of a validator's beta basket on a subnet, realizing
/// `tao` which was staked onto their root position.
BasketClaimed {
/// Validator hotkey the basket belongs to.
hotkey: T::AccountId,
/// Staker coldkey that claimed.
coldkey: T::AccountId,
/// Subnet the basket alpha was redeemed from.
netuid: NetUid,
/// TAO realized and staked on root for the staker.
tao: TaoBalance,
},

/// A validator's beta basket on a dissolving subnet was liquidated back to its root
/// stakers, realizing `tao` distributed to the validator's root nominators.
BasketLiquidated {
/// Validator hotkey whose basket was liquidated.
hotkey: T::AccountId,
/// Subnet being dissolved.
netuid: NetUid,
/// TAO realized and credited to the validator's root stakers.
tao: TaoBalance,
},

/// Voting power tracking has been enabled for a subnet.
VotingPowerTrackingEnabled {
/// The subnet ID
Expand Down
4 changes: 3 additions & 1 deletion pallets/subtensor/src/macros/hooks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,9 @@ mod hooks {
// Capture the runtime-upgrade block for TAO-in refund cutover.
.saturating_add(migrations::migrate_tao_in_refund_deployment_block::migrate_tao_in_refund_deployment_block::<T>())
// Fix lock state left behind by subnet-scoped hotkey swaps.
.saturating_add(migrations::migrate_fix_subnet_hotkey_lock_swaps::migrate_fix_subnet_hotkey_lock_swaps::<T>());
.saturating_add(migrations::migrate_fix_subnet_hotkey_lock_swaps::migrate_fix_subnet_hotkey_lock_swaps::<T>())
// Seed the beta-basket escrow model from legacy RootClaimable state.
.saturating_add(migrations::migrate_seed_beta_basket::migrate_seed_beta_basket::<T>());
weight
}

Expand Down
Loading